0.8.3: Adventurer's Journal becomes the "what's available + where to go" view
Build / build (push) Has been cancelled

Rebuilt the Journal as a town-board view rather than just personal status.
Five sections, top to bottom:

1. Available Bounties — today's full pool of 5 with rewards + state
   markers (active/ready/cooldown). Header: "Visit the Bounty Officer
   at the Adventurer's Guild"
2. Plots For Sale — every plot with state (for sale / taken by X /
   yours). Header: "Visit the Plot Clerk at the Plot Office"
3. Exchange Rates — each wealth-token with list + your effective rate.
   Header: "Visit the Exchange Clerk at the Exchange"
4. Civic Tier — your current + next-tier cost. Header: "Visit the Civic
   Registrar at the Town Hall"
5. Your Status — active bounty progress bars, owned plot count, wealth
   summary

Each section header includes the NPC name + building so players know
which spawn building to walk to. The Journal itself is still read-only;
all transactions happen at the NPCs.

Content overflows the 360x220 screen, so the body uses a PanelScrollBar
(right edge) for vertical scrolling. Mouse wheel scrolls too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-08 18:30:03 +00:00
parent 14b845e9a8
commit 7bf721bf15
2 changed files with 102 additions and 60 deletions
+1 -1
View File
@@ -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
@@ -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.
*
* <p>Three sections, top to bottom:
* <p>Read-only. Five sections, top to bottom:
* <ul>
* <li><b>Active Bounties</b> — kills in progress, with progress bars
* and turn-in rewards. Primary use case.</li>
* <li><b>Exchange Rates</b> — 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.</li>
* <li><b>Owned Plots</b> — what you own + your slot cap.</li>
* <li><b>Available Bounties</b> — today's offered pool (5), see Bounty Officer</li>
* <li><b>Plots For Sale</b> — unsold plots, see Plot Clerk</li>
* <li><b>Exchange Rates</b> — wealth-token rates, see Exchange Clerk</li>
* <li><b>Civic Tier</b> — your tier + next cost, see Civic Registrar</li>
* <li><b>Your Status</b> — active bounties / owned plots / wealth summary</li>
* </ul>
*
* <p>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.
* <p>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.
*
* <p>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<String, ClientBnsState.ActiveBounty> 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<ResourceLocation, Long> 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<String, ClientBnsState.ActiveBounty> 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);
}
}
}