diff --git a/gradle.properties b/gradle.properties
index 1ed3a20..31256ee 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -19,5 +19,5 @@ loader_version_range=[1,)
mod_id=bnstoolkit
mod_name=Brass and Sigil Toolkit
mod_license=All Rights Reserved
-mod_version=0.8.2
+mod_version=0.8.3
mod_group_id=uk.sijbers.bnstoolkit
diff --git a/src/main/java/uk/sijbers/bnstoolkit/client/screens/QuestLogScreen.java b/src/main/java/uk/sijbers/bnstoolkit/client/screens/QuestLogScreen.java
index f4efd27..51249c5 100644
--- a/src/main/java/uk/sijbers/bnstoolkit/client/screens/QuestLogScreen.java
+++ b/src/main/java/uk/sijbers/bnstoolkit/client/screens/QuestLogScreen.java
@@ -1,10 +1,8 @@
package uk.sijbers.bnstoolkit.client.screens;
import dev.ftb.mods.ftblibrary.icon.Color4I;
-import dev.ftb.mods.ftblibrary.icon.Icon;
-import dev.ftb.mods.ftblibrary.icon.Icons;
import dev.ftb.mods.ftblibrary.ui.Panel;
-import dev.ftb.mods.ftblibrary.ui.SimpleTextButton;
+import dev.ftb.mods.ftblibrary.ui.PanelScrollBar;
import dev.ftb.mods.ftblibrary.ui.Theme;
import dev.ftb.mods.ftblibrary.ui.Widget;
import net.minecraft.client.gui.GuiGraphics;
@@ -12,30 +10,30 @@ import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import uk.sijbers.bnstoolkit.client.ClientBnsState;
import uk.sijbers.bnstoolkit.data.Bounty;
+import uk.sijbers.bnstoolkit.data.CivicTier;
import uk.sijbers.bnstoolkit.data.Plot;
import uk.sijbers.bnstoolkit.data.SellCatalogue;
-import uk.sijbers.bnstoolkit.network.NetSend;
import java.util.Map;
/**
- * Adventurer's Journal — at-a-glance overview of the player's active
- * adventure. Read-only; no economy mutation happens here.
+ * Adventurer's Journal — what's available in town and what you've got.
*
- *
Three sections, top to bottom:
+ *
Read-only. Five sections, top to bottom:
*
- * - Active Bounties — kills in progress, with progress bars
- * and turn-in rewards. Primary use case.
- * - Exchange Rates — current per-unit spur payout for
- * each wealth-token after the player's tier fee + lifetime DR.
- * Lets players check rates from the field without travelling to
- * the Exchange.
- * - Owned Plots — what you own + your slot cap.
+ * - Available Bounties — today's offered pool (5), see Bounty Officer
+ * - Plots For Sale — unsold plots, see Plot Clerk
+ * - Exchange Rates — wealth-token rates, see Exchange Clerk
+ * - Civic Tier — your tier + next cost, see Civic Registrar
+ * - Your Status — active bounties / owned plots / wealth summary
*
*
- * Not to be confused with FTB Quests, which is a separate
- * chapter-based progression mod with its own Quest Book item. A footer
- * line points players there.
+ *
Each section heads with "Visit X at Y" so players know which NPC at
+ * spawn to interact with. The Journal itself doesn't transact —
+ * everything below is informational.
+ *
+ *
Content overflows the screen; a {@link PanelScrollBar} on the right
+ * lets the player scroll through.
*/
public final class QuestLogScreen extends BaseBnsScreen {
public QuestLogScreen() {
@@ -46,76 +44,123 @@ public final class QuestLogScreen extends BaseBnsScreen {
public void addWidgets() {
ContentPanel content = new ContentPanel(this);
content.setPos(8, contentTop());
- content.setSize(getWidth() - 16, getHeight() - 50);
+ content.setSize(getWidth() - 24, getHeight() - 50); // -24 to leave room for scrollbar
+ content.setOnlyRenderWidgetsInside(true);
add(content);
+ // Vertical scrollbar to the right of the content panel
+ PanelScrollBar scrollBar = new PanelScrollBar(this, content);
+ scrollBar.setPos(getWidth() - 14, contentTop());
+ scrollBar.setSize(6, getHeight() - 50);
+ add(scrollBar);
+
add(BaseBnsScreen.makeBackOrClose(this));
}
@Override
public void alignWidgets() {
- Widget close = getWidgets().get(1);
+ Widget close = getWidgets().get(2);
close.setSize(72, 16);
close.setPos(getWidth() - 80, getHeight() - 22);
}
- /** Three-section journal body. */
+ /** Scrollable journal body. */
private static final class ContentPanel extends Panel {
ContentPanel(Panel parent) { super(parent); }
@Override public void addWidgets() {
- // Section 1: Active Bounties
- int activeCount = ClientBnsState.activeBounties.size();
- int cap = ClientBnsState.bountySlotCap;
- add(new Header(this,"§e=== Active Bounties (" + activeCount + " / " + cap + ") ==="));
- if (ClientBnsState.activeBounties.isEmpty()) {
- add(new Header(this,"§8 none — visit the Bounty Board at spawn"));
+ // ─── 1. Available Bounties ─────────────────────────────────
+ add(new Header(this, "§e=== Available Bounties ==="));
+ add(new Header(this, "§7Visit the Bounty Officer at the Adventurer's Guild"));
+ if (ClientBnsState.dailyPoolIds.isEmpty()) {
+ add(new Header(this, "§8 (none rolled yet — log in to refresh)"));
} else {
- for (Map.Entry e : ClientBnsState.activeBounties.entrySet()) {
- Bounty b = Bounty.byId(e.getKey());
+ int shown = 0;
+ for (String id : ClientBnsState.dailyPoolIds) {
+ Bounty b = Bounty.byId(id);
if (b == null) continue;
- ClientBnsState.ActiveBounty ab = e.getValue();
- String bar = progressBar(ab.progress(), ab.target(), 10);
- String line = " " + b.name() + " " + bar
- + " §7" + ab.progress() + "/" + ab.target()
- + "§r §6+" + ab.reward() + "§r";
- add(new Header(this,line));
+ Bounty.State state = ClientBnsState.stateOf(id);
+ String prefix;
+ switch (state) {
+ case ACTIVE -> prefix = "§e●§r ";
+ case READY -> prefix = "§a✓§r ";
+ case COOLDOWN -> prefix = "§8○§r ";
+ default -> prefix = "§7○§r ";
+ }
+ add(new Header(this, " " + prefix + b.name() + " §6+" + b.reward() + " sp§r"));
+ shown++;
}
+ if (shown == 0) add(new Header(this, "§8 (pool empty — log in to refresh)"));
}
+ add(new Header(this, ""));
- add(new Header(this,""));
+ // ─── 2. Plots For Sale ─────────────────────────────────────
+ add(new Header(this, "§e=== Plots For Sale ==="));
+ add(new Header(this, "§7Visit the Plot Clerk at the Plot Office"));
+ int forSale = 0, taken = 0, mine = 0;
+ for (Plot p : Plot.all()) {
+ String owner = ClientBnsState.ownerOf(p.id());
+ boolean isMine = ClientBnsState.ownsPlot(p.id());
+ String marker;
+ String suffix;
+ if (isMine) { marker = "§a★§r "; suffix = "§a yours§r"; mine++; }
+ else if (!owner.isEmpty()) { marker = "§c●§r "; suffix = "§c taken by " + owner + "§r"; taken++; }
+ else { marker = "§e○§r "; suffix = "§6 " + p.price() + " sp§r"; forSale++; }
+ add(new Header(this, " " + marker + p.name() + " §7(" + p.size() + ")§r" + suffix));
+ }
+ if (Plot.all().isEmpty()) {
+ add(new Header(this, "§8 (no plots defined yet)"));
+ }
+ add(new Header(this, ""));
- // Section 2: Exchange rates
- add(new Header(this,"§e=== Exchange Rates ==="));
+ // ─── 3. Exchange Rates ─────────────────────────────────────
+ add(new Header(this, "§e=== Exchange Rates ==="));
+ add(new Header(this, "§7Visit the Exchange Clerk at the Exchange"));
int fee = ClientBnsState.effectiveFeePct;
- add(new Header(this,"§7 Your tier fee: §c" + fee + "%§r"));
+ add(new Header(this, "§7Your tier fee: §c" + fee + "%§r"));
for (Map.Entry entry : SellCatalogue.PRICES.entrySet()) {
ResourceLocation id = entry.getKey();
long base = entry.getValue();
long sold = ClientBnsState.lifetimeSoldOf(id);
long current = SellCatalogue.singleUnitPriceDisplay(id, sold, fee);
- String name = displayName(id);
- String line = String.format(" %s§r §7%d§r → §a%d§r spurs", name, base, current);
- if (sold > 0) line += " §8(sold " + sold + ")§r";
- add(new Header(this,line));
+ String line = String.format(" %s§r §7list %d§r → §a%d sp/unit§r", displayName(id), base, current);
+ add(new Header(this, line));
}
+ add(new Header(this, ""));
- add(new Header(this,""));
-
- // Section 3: Owned Plots
- int owned = ClientBnsState.ownedPlotIds.size();
- add(new Header(this,"§e=== Owned Plots (" + owned + " / " + ClientBnsState.plotSlotCap + ") ==="));
- if (ClientBnsState.ownedPlotIds.isEmpty()) {
- add(new Header(this,"§8 none — visit the Plot Office at spawn"));
+ // ─── 4. Civic Tier ─────────────────────────────────────────
+ add(new Header(this, "§e=== Civic Tier ==="));
+ add(new Header(this, "§7Visit the Civic Registrar at the Town Hall"));
+ CivicTier cur = CivicTier.byId(ClientBnsState.tier);
+ add(new Header(this, " Current: §" + cur.colour().getChar() + cur.name() + "§r §7(tier "
+ + cur.id() + "/" + CivicTier.MAX + ")§r"));
+ if (cur.id() < CivicTier.MAX) {
+ CivicTier next = CivicTier.byId(cur.id() + 1);
+ add(new Header(this, " Next: §" + next.colour().getChar() + next.name()
+ + "§r — §6" + next.cost() + " sp§r"));
} else {
- for (String pid : ClientBnsState.ownedPlotIds) {
- Plot p = Plot.byId(pid);
- if (p != null) add(new Header(this," §a★§r " + p.name() + " §7(" + p.size() + ")§r"));
- }
+ add(new Header(this, " §8(you have reached the highest civic rank)§r"));
}
+ add(new Header(this, ""));
- add(new Header(this,""));
- add(new Header(this,"§8FTB Quests is a separate mod — open your Quest Book item for chapters."));
+ // ─── 5. Your Status ────────────────────────────────────────
+ add(new Header(this, "§e=== Your Status ==="));
+ int activeBounties = ClientBnsState.activeBounties.size();
+ add(new Header(this, " Active bounties: §f" + activeBounties + " / " + ClientBnsState.bountySlotCap));
+ for (Map.Entry e : ClientBnsState.activeBounties.entrySet()) {
+ Bounty b = Bounty.byId(e.getKey());
+ if (b == null) continue;
+ ClientBnsState.ActiveBounty ab = e.getValue();
+ String bar = progressBar(ab.progress(), ab.target(), 10);
+ add(new Header(this, " " + b.name() + " " + bar + " §7"
+ + ab.progress() + "/" + ab.target() + "§r"));
+ }
+ add(new Header(this, " Owned plots: §f" + mine + " / " + ClientBnsState.plotSlotCap));
+ add(new Header(this, " Wealth: §6" + (ClientBnsState.cashOnHand + ClientBnsState.bankBalance)
+ + " sp§r §7(cash " + ClientBnsState.cashOnHand
+ + " + bank " + ClientBnsState.bankBalance + ")§r"));
+ add(new Header(this, ""));
+ add(new Header(this, "§8FTB Quests is a separate mod — open your Quest Book item for chapters."));
}
@Override public void alignWidgets() {
@@ -138,9 +183,7 @@ public final class QuestLogScreen extends BaseBnsScreen {
}
private static String displayName(ResourceLocation id) {
- String path = id.getPath();
- // Friendly-cased name from the id: "ancient_debris" -> "Ancient Debris"
- String[] parts = path.split("_");
+ String[] parts = id.getPath().split("_");
StringBuilder out = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
if (parts[i].isEmpty()) continue;
@@ -159,5 +202,4 @@ public final class QuestLogScreen extends BaseBnsScreen {
theme.drawString(gfx, Component.literal(text), x + 2, y + 2, Color4I.WHITE, Theme.SHADOW);
}
}
-
}