0.2.0: feature-complete UI — 5 FTB-Library screens + HUD overlay
Build / build (push) Has been cancelled
Build / build (push) Has been cancelled
All five screens implemented with FTB Library widgets (Panel/Widget/
SimpleTextButton). Layout matches FTB Quests/Chunks for visual
consistency with the rest of the pack.
Screens:
- TierUpgradeScreen — 13-row ladder, current tier highlighted,
Upgrade button to next tier with cost/affordability check
- ShopBrowserScreen — 9-cell item grid + detail pane, quick-sell
buttons (1/16/64) with live DR-aware price preview
- BountyBoardScreen — daily pool + active set, accept/cancel/turn-in
per state, slot-cap indicator
- PlotPurchaseScreen — plot list + detail pane, buy/release with
ownership colour-coding and tier-gated buy
- QuestLogScreen — at-a-glance tier+balance summary, active
bounties with progress bars, owned plots
HUD overlay: top-left tier name + spurs balance, registered as a
NeoForge GUI layer above the experience bar. 1Hz server push keeps
the balance current without polling client-side inventory.
Network: replaces single smoke-test packet with 7-packet contract:
- C2S: RequestEconomySnapshot, RunBnsCommand (whitelisted)
- S2C: TierStateUpdate, SellSnapshot, BountySnapshot, PlotSnapshot, Toast
Server-side bridge (ServerEconomyBridge) reads player NBT and
Numismatics-by-item-id-string for balance, routes write verbs
through the existing KubeJS /bns chat commands so KubeJS remains the
source of truth for economy logic.
Key bindings (rebindable in vanilla controls menu):
- K Civic tier ladder
- J Bounty board
- N Plot office
- H Bazaar shop
- M Quest log
FTB Library + Architectury maven repos wired in build.gradle as
compileOnly; the jar stays small since FTB Library is already in pack.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+13
-11
@@ -19,17 +19,16 @@ sourceSets.main.resources {
|
|||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
// FTB Library / FTB Chunks / FTB Quests live here. Used compileOnly
|
|
||||||
// when we wire screens to the FTB UI framework (M1+).
|
|
||||||
maven {
|
maven {
|
||||||
name = 'FTB'
|
name = 'FTB'
|
||||||
url = 'https://maven.ftb.dev/releases'
|
url = 'https://maven.ftb.dev/releases'
|
||||||
content { includeGroup 'dev.ftb.mods' }
|
content { includeGroup 'dev.ftb.mods' }
|
||||||
}
|
}
|
||||||
// Numismatics + Create dependencies live on Modrinth's maven mirror.
|
// Architectury (transitive of FTB Library)
|
||||||
maven {
|
maven {
|
||||||
name = 'ModrinthMaven'
|
name = 'Architectury'
|
||||||
url = 'https://api.modrinth.com/maven'
|
url = 'https://maven.architectury.dev/'
|
||||||
|
content { includeGroup 'dev.architectury' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,12 +81,15 @@ configurations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// TODO(matt M1): wire FTB Library compileOnly dep when first FTB-UI screen lands.
|
// FTB Library: provides the Panel/Widget/Button framework used by every
|
||||||
// Candidate coord (verify version pinning against pack):
|
// bnstoolkit screen. compileOnly because it's shipped in the modpack
|
||||||
// compileOnly "dev.ftb.mods:ftb-library-neoforge:2101.1.31"
|
// already — we don't want to ship a second copy in our jar. Version
|
||||||
//
|
// pinned to the live pack's installed version (2101.1.31).
|
||||||
// TODO(matt M2): Numismatics compileOnly dep for the shop block integration.
|
compileOnly "dev.ftb.mods:ftb-library-neoforge:2101.1.31"
|
||||||
// Modrinth maven path: maven.modrinth/numismatics/<version>
|
|
||||||
|
// No Numismatics compileOnly dep — we read coin balances by inventory-
|
||||||
|
// scanning items via their string IDs ("numismatics:spur", etc) which
|
||||||
|
// doesn't need any of Numismatics' Java API on the classpath.
|
||||||
}
|
}
|
||||||
|
|
||||||
var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) {
|
var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) {
|
||||||
|
|||||||
+1
-1
@@ -19,5 +19,5 @@ loader_version_range=[1,)
|
|||||||
mod_id=bnstoolkit
|
mod_id=bnstoolkit
|
||||||
mod_name=Brass and Sigil Toolkit
|
mod_name=Brass and Sigil Toolkit
|
||||||
mod_license=All Rights Reserved
|
mod_license=All Rights Reserved
|
||||||
mod_version=0.1.1
|
mod_version=0.2.0
|
||||||
mod_group_id=uk.sijbers.bnstoolkit
|
mod_group_id=uk.sijbers.bnstoolkit
|
||||||
|
|||||||
@@ -1,22 +1,31 @@
|
|||||||
package uk.sijbers.bnstoolkit;
|
package uk.sijbers.bnstoolkit;
|
||||||
|
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent;
|
||||||
import net.neoforged.fml.common.Mod;
|
import net.neoforged.fml.common.Mod;
|
||||||
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
|
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
|
||||||
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
|
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
|
||||||
|
import net.neoforged.neoforge.event.tick.PlayerTickEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import uk.sijbers.bnstoolkit.network.BnsNetwork;
|
import uk.sijbers.bnstoolkit.network.BnsNetwork;
|
||||||
|
import uk.sijbers.bnstoolkit.network.ServerEconomyBridge;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Brass and Sigil Toolkit — common (client+server) mod entry.
|
* Brass and Sigil Toolkit — common (client+server) mod entry.
|
||||||
*
|
*
|
||||||
* Mod ID matches {@link #MOD_ID}. The matching value lives in
|
* Two server-side responsibilities live here:
|
||||||
* gradle.properties (mod_id=bnstoolkit) and is interpolated into
|
* 1. Push a full economy snapshot to every player on login so screens
|
||||||
* src/main/templates/META-INF/neoforge.mods.toml at build time.
|
* have data the first time they're opened.
|
||||||
|
* 2. Push a TierStateUpdatePacket every 20 ticks (1s) to refresh the
|
||||||
|
* HUD spurs balance — KubeJS doesn't fire an event when coins move
|
||||||
|
* in the player's inventory, so we have to poll.
|
||||||
*
|
*
|
||||||
* Scope (M0): empty scaffold. Registers no items, blocks, or screens yet.
|
* The polling tick is cheap (one SpurBalance.totalSpurs sweep over 41
|
||||||
* Just loads cleanly on client and dedicated server so the deploy pipeline
|
* inventory slots) and avoids forcing the player to interact with a
|
||||||
* works end-to-end before we start adding real surface area.
|
* shop/quest to see their wallet update.
|
||||||
*/
|
*/
|
||||||
@Mod(BnsToolkit.MOD_ID)
|
@Mod(BnsToolkit.MOD_ID)
|
||||||
public final class BnsToolkit {
|
public final class BnsToolkit {
|
||||||
@@ -29,14 +38,32 @@ public final class BnsToolkit {
|
|||||||
BnsNetwork.register(modBus);
|
BnsNetwork.register(modBus);
|
||||||
|
|
||||||
modBus.addListener(this::onCommonSetup);
|
modBus.addListener(this::onCommonSetup);
|
||||||
|
NeoForge.EVENT_BUS.register(this);
|
||||||
// NOTE: do NOT call NeoForge.EVENT_BUS.register(this) here — NeoForge
|
|
||||||
// 21.1+ throws if the registered class has zero @SubscribeEvent methods.
|
|
||||||
// Add the register() call back together with the first @SubscribeEvent
|
|
||||||
// method (likely PlayerEvent.PlayerLoggedInEvent in M1).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onCommonSetup(final FMLCommonSetupEvent event) {
|
private void onCommonSetup(final FMLCommonSetupEvent event) {
|
||||||
LOG.info("[bnstoolkit] common setup complete (M0 scaffold — no surface yet)");
|
LOG.info("[bnstoolkit] common setup complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
|
||||||
|
if (event.getEntity() instanceof ServerPlayer sp) {
|
||||||
|
ServerEconomyBridge.pushSnapshot(sp, "ALL");
|
||||||
|
LOG.info("[bnstoolkit] pushed initial economy snapshot to {}", sp.getName().getString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int tickCounter = 0;
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public void onPlayerTick(PlayerTickEvent.Post event) {
|
||||||
|
// 1 Hz balance push so the HUD reflects coin-pickup without polling
|
||||||
|
// the inventory client-side.
|
||||||
|
if (event.getEntity() instanceof ServerPlayer sp) {
|
||||||
|
tickCounter++;
|
||||||
|
if (tickCounter % 20 == 0) {
|
||||||
|
ServerEconomyBridge.pushTierUpdate(sp);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,10 @@ import net.neoforged.api.distmarker.Dist;
|
|||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
import net.neoforged.fml.common.Mod;
|
import net.neoforged.fml.common.Mod;
|
||||||
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
|
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
|
||||||
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
import uk.sijbers.bnstoolkit.client.BnsKeyBindings;
|
import uk.sijbers.bnstoolkit.client.BnsKeyBindings;
|
||||||
|
import uk.sijbers.bnstoolkit.client.hud.SpurHud;
|
||||||
|
|
||||||
/**
|
|
||||||
* Client-only mod entry. Hosts key bindings + screen registration so the
|
|
||||||
* dedicated server never loads them.
|
|
||||||
*
|
|
||||||
* Activated via the {@code dist = Dist.CLIENT} parameter on the @Mod
|
|
||||||
* annotation — NeoForge will skip this class entirely on the server side.
|
|
||||||
*/
|
|
||||||
@Mod(value = BnsToolkit.MOD_ID, dist = Dist.CLIENT)
|
@Mod(value = BnsToolkit.MOD_ID, dist = Dist.CLIENT)
|
||||||
public final class BnsToolkitClient {
|
public final class BnsToolkitClient {
|
||||||
|
|
||||||
@@ -20,11 +15,14 @@ public final class BnsToolkitClient {
|
|||||||
BnsToolkit.LOG.info("[bnstoolkit] client entry constructed");
|
BnsToolkit.LOG.info("[bnstoolkit] client entry constructed");
|
||||||
modBus.addListener(this::onClientSetup);
|
modBus.addListener(this::onClientSetup);
|
||||||
modBus.addListener(BnsKeyBindings::onRegisterKeyMappings);
|
modBus.addListener(BnsKeyBindings::onRegisterKeyMappings);
|
||||||
|
modBus.addListener(SpurHud::onRegisterGuiLayers);
|
||||||
|
|
||||||
|
// Client-tick listener (consumeClick on key mappings) is a @SubscribeEvent
|
||||||
|
// on BnsKeyBindings — register the class on the global event bus.
|
||||||
|
NeoForge.EVENT_BUS.register(BnsKeyBindings.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onClientSetup(final FMLClientSetupEvent event) {
|
private void onClientSetup(final FMLClientSetupEvent event) {
|
||||||
// TODO(matt M1): register screens here when bnstoolkit ships its first GUI.
|
|
||||||
// MenuScreens.register(BnsMenus.TIER_UPGRADE.get(), TierUpgradeScreen::new);
|
|
||||||
BnsToolkit.LOG.info("[bnstoolkit] client setup complete");
|
BnsToolkit.LOG.info("[bnstoolkit] client setup complete");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,32 @@ package uk.sijbers.bnstoolkit.client;
|
|||||||
|
|
||||||
import com.mojang.blaze3d.platform.InputConstants;
|
import com.mojang.blaze3d.platform.InputConstants;
|
||||||
import net.minecraft.client.KeyMapping;
|
import net.minecraft.client.KeyMapping;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent;
|
||||||
import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent;
|
import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent;
|
||||||
|
import net.neoforged.neoforge.client.event.ClientTickEvent;
|
||||||
import net.neoforged.neoforge.client.settings.KeyConflictContext;
|
import net.neoforged.neoforge.client.settings.KeyConflictContext;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
import uk.sijbers.bnstoolkit.BnsToolkit;
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.client.screens.BountyBoardScreen;
|
||||||
|
import uk.sijbers.bnstoolkit.client.screens.PlotPurchaseScreen;
|
||||||
|
import uk.sijbers.bnstoolkit.client.screens.QuestLogScreen;
|
||||||
|
import uk.sijbers.bnstoolkit.client.screens.ShopBrowserScreen;
|
||||||
|
import uk.sijbers.bnstoolkit.client.screens.TierUpgradeScreen;
|
||||||
|
import uk.sijbers.bnstoolkit.network.NetSend;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key bindings for the toolkit screens. All keys are CLIENT-only and live
|
* Key bindings for the toolkit screens.
|
||||||
* under a "Brass and Sigil" category in the controls menu.
|
|
||||||
*
|
*
|
||||||
* Default assignments are placeholders — picked to avoid known FTB
|
* Default keys avoid known FTB Quests / Chunks / Inventory defaults:
|
||||||
* Library / FTB Chunks / Numismatics defaults. Players can rebind in the
|
* K — Civic tier ladder (replaces vanilla advancements which is L)
|
||||||
|
* J — Bounty board
|
||||||
|
* N — Plot map / office
|
||||||
|
* M — Quest log overview
|
||||||
|
*
|
||||||
|
* Picked so they sit on the home row right-hand side and don't clash
|
||||||
|
* with WASD movement / vanilla / FTB defaults. Player can rebind in the
|
||||||
* vanilla controls menu like any other mod.
|
* vanilla controls menu like any other mod.
|
||||||
*
|
|
||||||
* Wiring: each binding gets polled in a {@code ClientTickEvent} handler
|
|
||||||
* once the corresponding screen is implemented; M0 keeps them inert.
|
|
||||||
*/
|
*/
|
||||||
public final class BnsKeyBindings {
|
public final class BnsKeyBindings {
|
||||||
private BnsKeyBindings() {}
|
private BnsKeyBindings() {}
|
||||||
@@ -26,25 +37,65 @@ public final class BnsKeyBindings {
|
|||||||
public static final KeyMapping OPEN_TIER = new KeyMapping(
|
public static final KeyMapping OPEN_TIER = new KeyMapping(
|
||||||
"key." + BnsToolkit.MOD_ID + ".tier",
|
"key." + BnsToolkit.MOD_ID + ".tier",
|
||||||
KeyConflictContext.IN_GAME,
|
KeyConflictContext.IN_GAME,
|
||||||
InputConstants.Type.KEYSYM.getOrCreate(GLFW.GLFW_KEY_T),
|
InputConstants.Type.KEYSYM.getOrCreate(GLFW.GLFW_KEY_K),
|
||||||
CATEGORY);
|
CATEGORY);
|
||||||
|
|
||||||
public static final KeyMapping OPEN_BOUNTIES = new KeyMapping(
|
public static final KeyMapping OPEN_BOUNTIES = new KeyMapping(
|
||||||
"key." + BnsToolkit.MOD_ID + ".bounties",
|
"key." + BnsToolkit.MOD_ID + ".bounties",
|
||||||
KeyConflictContext.IN_GAME,
|
KeyConflictContext.IN_GAME,
|
||||||
InputConstants.Type.KEYSYM.getOrCreate(GLFW.GLFW_KEY_B),
|
InputConstants.Type.KEYSYM.getOrCreate(GLFW.GLFW_KEY_J),
|
||||||
CATEGORY);
|
CATEGORY);
|
||||||
|
|
||||||
public static final KeyMapping OPEN_PLOTS = new KeyMapping(
|
public static final KeyMapping OPEN_PLOTS = new KeyMapping(
|
||||||
"key." + BnsToolkit.MOD_ID + ".plots",
|
"key." + BnsToolkit.MOD_ID + ".plots",
|
||||||
KeyConflictContext.IN_GAME,
|
KeyConflictContext.IN_GAME,
|
||||||
InputConstants.Type.KEYSYM.getOrCreate(GLFW.GLFW_KEY_P),
|
InputConstants.Type.KEYSYM.getOrCreate(GLFW.GLFW_KEY_N),
|
||||||
|
CATEGORY);
|
||||||
|
|
||||||
|
public static final KeyMapping OPEN_SHOP = new KeyMapping(
|
||||||
|
"key." + BnsToolkit.MOD_ID + ".shop",
|
||||||
|
KeyConflictContext.IN_GAME,
|
||||||
|
InputConstants.Type.KEYSYM.getOrCreate(GLFW.GLFW_KEY_H),
|
||||||
|
CATEGORY);
|
||||||
|
|
||||||
|
public static final KeyMapping OPEN_QUESTLOG = new KeyMapping(
|
||||||
|
"key." + BnsToolkit.MOD_ID + ".questlog",
|
||||||
|
KeyConflictContext.IN_GAME,
|
||||||
|
InputConstants.Type.KEYSYM.getOrCreate(GLFW.GLFW_KEY_M),
|
||||||
CATEGORY);
|
CATEGORY);
|
||||||
|
|
||||||
public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) {
|
public static void onRegisterKeyMappings(RegisterKeyMappingsEvent event) {
|
||||||
event.register(OPEN_TIER);
|
event.register(OPEN_TIER);
|
||||||
event.register(OPEN_BOUNTIES);
|
event.register(OPEN_BOUNTIES);
|
||||||
event.register(OPEN_PLOTS);
|
event.register(OPEN_PLOTS);
|
||||||
BnsToolkit.LOG.info("[bnstoolkit] registered 3 key bindings under {}", CATEGORY);
|
event.register(OPEN_SHOP);
|
||||||
|
event.register(OPEN_QUESTLOG);
|
||||||
|
BnsToolkit.LOG.info("[bnstoolkit] registered 5 key bindings under {}", CATEGORY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onClientTick(ClientTickEvent.Post event) {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc == null || mc.player == null || mc.screen != null) return;
|
||||||
|
if (OPEN_TIER.consumeClick()) {
|
||||||
|
NetSend.requestSnapshot("TIER");
|
||||||
|
new TierUpgradeScreen().openGui();
|
||||||
|
}
|
||||||
|
if (OPEN_BOUNTIES.consumeClick()) {
|
||||||
|
NetSend.requestSnapshot("BOUNTY");
|
||||||
|
new BountyBoardScreen().openGui();
|
||||||
|
}
|
||||||
|
if (OPEN_PLOTS.consumeClick()) {
|
||||||
|
NetSend.requestSnapshot("PLOT");
|
||||||
|
new PlotPurchaseScreen().openGui();
|
||||||
|
}
|
||||||
|
if (OPEN_SHOP.consumeClick()) {
|
||||||
|
NetSend.requestSnapshot("SELL");
|
||||||
|
new ShopBrowserScreen().openGui();
|
||||||
|
}
|
||||||
|
if (OPEN_QUESTLOG.consumeClick()) {
|
||||||
|
NetSend.requestSnapshot("ALL");
|
||||||
|
new QuestLogScreen().openGui();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.client;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import uk.sijbers.bnstoolkit.data.Bounty;
|
||||||
|
import uk.sijbers.bnstoolkit.data.Plot;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client-side cache of the player's economy state, populated by S2C
|
||||||
|
* packets. Read by the HUD overlay every frame and by every open screen.
|
||||||
|
*
|
||||||
|
* The server pushes:
|
||||||
|
* - {@link uk.sijbers.bnstoolkit.network.s2c.TierStateUpdatePacket} — tier + balance
|
||||||
|
* - {@link uk.sijbers.bnstoolkit.network.s2c.SellSnapshotPacket} — lifetime-sold map + effective fee
|
||||||
|
* - {@link uk.sijbers.bnstoolkit.network.s2c.BountySnapshotPacket} — today's pool + active set
|
||||||
|
* - {@link uk.sijbers.bnstoolkit.network.s2c.PlotSnapshotPacket} — ownership map
|
||||||
|
*
|
||||||
|
* All access is single-threaded (client tick / render thread) so no locks.
|
||||||
|
*/
|
||||||
|
public final class ClientBnsState {
|
||||||
|
private ClientBnsState() {}
|
||||||
|
|
||||||
|
public static volatile int tier = 1;
|
||||||
|
public static volatile long spurs = 0L;
|
||||||
|
public static volatile int effectiveFeePct = 25;
|
||||||
|
|
||||||
|
// SellSnapshot:
|
||||||
|
public static final Map<ResourceLocation, Long> lifetimeSold = new HashMap<>();
|
||||||
|
|
||||||
|
// BountySnapshot:
|
||||||
|
public static final List<String> dailyPoolIds = new ArrayList<>();
|
||||||
|
public static final Map<String, ActiveBounty> activeBounties = new HashMap<>();
|
||||||
|
public static final List<String> completedIds = new ArrayList<>();
|
||||||
|
public static volatile int bountySlotCap = 3;
|
||||||
|
|
||||||
|
// PlotSnapshot:
|
||||||
|
public static final Map<String, String> plotOwners = new HashMap<>(); // plot id -> owner name (or "" if free)
|
||||||
|
public static volatile int plotSlotCap = 0;
|
||||||
|
public static final List<String> ownedPlotIds = new ArrayList<>();
|
||||||
|
|
||||||
|
public static long lifetimeSoldOf(ResourceLocation item) {
|
||||||
|
Long v = lifetimeSold.get(item);
|
||||||
|
return v == null ? 0L : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bounty.State stateOf(String bountyId) {
|
||||||
|
if (completedIds.contains(bountyId)) return Bounty.State.COOLDOWN;
|
||||||
|
ActiveBounty ab = activeBounties.get(bountyId);
|
||||||
|
if (ab == null) return Bounty.State.AVAILABLE;
|
||||||
|
return ab.progress >= ab.target ? Bounty.State.READY : Bounty.State.ACTIVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean ownsPlot(String plotId) {
|
||||||
|
return ownedPlotIds.contains(plotId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String ownerOf(String plotId) {
|
||||||
|
return plotOwners.getOrDefault(plotId, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Plot plotById(String id) {
|
||||||
|
return Plot.byId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ActiveBounty(int progress, int target, long reward) {}
|
||||||
|
}
|
||||||
@@ -1,30 +1,69 @@
|
|||||||
package uk.sijbers.bnstoolkit.client.hud;
|
package uk.sijbers.bnstoolkit.client.hud;
|
||||||
|
|
||||||
import net.minecraft.client.DeltaTracker;
|
import net.minecraft.client.DeltaTracker;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.client.gui.LayeredDraw;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.neoforged.neoforge.client.event.RegisterGuiLayersEvent;
|
||||||
|
import net.neoforged.neoforge.client.gui.VanillaGuiLayers;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.client.ClientBnsState;
|
||||||
|
import uk.sijbers.bnstoolkit.client.screens.TierUpgradeScreen;
|
||||||
|
import uk.sijbers.bnstoolkit.data.CivicTier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tiny on-screen overlay showing the player's spurs balance + civic tier.
|
* Persistent on-screen overlay: civic tier + spurs balance, top-left.
|
||||||
*
|
*
|
||||||
* Spec ref: docs/bnstoolkit-architecture.md §4.6 (HUD overlay)
|
* Drawn as a NeoForge {@link RegisterGuiLayersEvent} layer rather than a
|
||||||
*
|
* {@code RenderGuiEvent} subscriber so it composes cleanly with the
|
||||||
* TODO(matt M4): wire this up via {@code RegisterGuiLayersEvent} when the
|
* vanilla layer system (and hides automatically when chat / inventory
|
||||||
* NeoForge gui-layer registration lands. The signature here matches the
|
* suppresses HUD).
|
||||||
* {@code LayeredDraw.Layer} contract (a render method taking GuiGraphics +
|
|
||||||
* DeltaTracker), so the eventual implementation only has to add the
|
|
||||||
* "implements" clause and the registration call.
|
|
||||||
*
|
|
||||||
* Position default: top-left, below buff icons. Player can disable in mod
|
|
||||||
* settings (no settings screen yet — comes with the FTB Library swap).
|
|
||||||
*/
|
*/
|
||||||
public final class SpurHud {
|
public final class SpurHud {
|
||||||
public static final SpurHud INSTANCE = new SpurHud();
|
|
||||||
|
|
||||||
private SpurHud() {}
|
private SpurHud() {}
|
||||||
|
|
||||||
public void render(GuiGraphics graphics, DeltaTracker delta) {
|
public static final SpurHud INSTANCE = new SpurHud();
|
||||||
// TODO(matt M4): draw "<tier-icon> <tier-name> <spurs> spurs"
|
public static final ResourceLocation LAYER_ID =
|
||||||
// Data source: client-side cache populated by S2C TierStateUpdate
|
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "spur_hud");
|
||||||
// packets the server emits whenever balance or tier changes.
|
|
||||||
|
public static void onRegisterGuiLayers(RegisterGuiLayersEvent event) {
|
||||||
|
event.registerAbove(VanillaGuiLayers.EXPERIENCE_BAR, LAYER_ID, INSTANCE::render);
|
||||||
|
BnsToolkit.LOG.info("[bnstoolkit] HUD layer registered as {}", LAYER_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void render(GuiGraphics gfx, DeltaTracker delta) {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc == null || mc.player == null) return;
|
||||||
|
if (mc.options.hideGui) return;
|
||||||
|
|
||||||
|
CivicTier cfg = CivicTier.byId(ClientBnsState.tier);
|
||||||
|
String tierLine = "§" + cfg.colour().getChar() + cfg.name() + "§r";
|
||||||
|
String spursLine = "§6" + TierUpgradeScreen.formatSpurs(ClientBnsState.spurs) + " spurs§r";
|
||||||
|
|
||||||
|
int x = 4;
|
||||||
|
int y = 4;
|
||||||
|
int w = Math.max(mc.font.width(tierLine), mc.font.width(spursLine)) + 8;
|
||||||
|
int h = 24;
|
||||||
|
|
||||||
|
gfx.fill(x, y, x + w, y + h, 0x80000000);
|
||||||
|
gfx.fill(x, y, x + w, y + 1, 0xFF666666);
|
||||||
|
gfx.fill(x, y + h - 1, x + w, y + h, 0xFF666666);
|
||||||
|
gfx.fill(x, y, x + 1, y + h, 0xFF666666);
|
||||||
|
gfx.fill(x + w - 1, y, x + w, y + h, 0xFF666666);
|
||||||
|
|
||||||
|
gfx.drawString(mc.font, Component.literal(tierLine), x + 4, y + 4, 0xFFFFFFFF, true);
|
||||||
|
gfx.drawString(mc.font, Component.literal(spursLine), x + 4, y + 14, 0xFFFFFFFF, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kept for symmetry with the LayeredDraw.Layer interface in case other
|
||||||
|
// code wants to call the render directly.
|
||||||
|
public void renderRaw(GuiGraphics gfx, DeltaTracker delta) {
|
||||||
|
render(gfx, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference suppression so unused imports don't warn.
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private static final LayeredDraw.Layer UNUSED_REF = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,57 @@
|
|||||||
package uk.sijbers.bnstoolkit.client.screens;
|
package uk.sijbers.bnstoolkit.client.screens;
|
||||||
|
|
||||||
|
import dev.ftb.mods.ftblibrary.icon.Color4I;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.BaseScreen;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.Theme;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.WidgetType;
|
||||||
import net.minecraft.client.gui.GuiGraphics;
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.client.gui.screens.Screen;
|
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared base for all toolkit screens.
|
* Shared parent for every bnstoolkit screen.
|
||||||
*
|
*
|
||||||
* TODO(matt M1): swap parent to {@code dev.ftb.mods.ftblibrary.ui.BaseScreen}
|
* Inherits FTB Library's BaseScreen — that brings in the panel-style
|
||||||
* once the FTB Library compileOnly dependency is wired in build.gradle. The
|
* chrome, scroll handling, modal dialog support, and consistent visual
|
||||||
* vanilla {@link Screen} parent is a stand-in so M0 compiles without FTB
|
* theming with FTB Quests / FTB Chunks. We override drawBackground to
|
||||||
* Library on the classpath and we can ship a "loads cleanly" mod first.
|
* add a title bar on top of the standard FTB gui panel.
|
||||||
*
|
|
||||||
* When the swap lands, each subclass moves from
|
|
||||||
* {@code render(GuiGraphics, ...)} -> the FTB Lib panel/widget tree
|
|
||||||
* Visual styling (background tile, title bar, close button) comes for
|
|
||||||
* free from FTB Library, matching FTB Quests / FTB Chunks look-and-feel.
|
|
||||||
*/
|
*/
|
||||||
public abstract class BaseBnsScreen extends Screen {
|
public abstract class BaseBnsScreen extends BaseScreen {
|
||||||
|
public static final int WIDTH = 360;
|
||||||
|
public static final int HEIGHT = 220;
|
||||||
|
|
||||||
|
protected final Component screenTitle;
|
||||||
|
|
||||||
protected BaseBnsScreen(Component title) {
|
protected BaseBnsScreen(Component title) {
|
||||||
super(title);
|
this.screenTitle = title;
|
||||||
|
setWidth(WIDTH);
|
||||||
|
setHeight(HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
|
public boolean doesGuiPauseGame() {
|
||||||
this.renderBackground(graphics, mouseX, mouseY, partialTick);
|
|
||||||
super.render(graphics, mouseX, mouseY, partialTick);
|
|
||||||
// Centred title — placeholder styling.
|
|
||||||
graphics.drawCenteredString(this.font, this.title, this.width / 2, 16, 0xFFFFFFFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isPauseScreen() {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean drawDefaultBackground(GuiGraphics graphics) {
|
||||||
|
return true; // dim the world behind
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drawBackground(GuiGraphics graphics, Theme theme, int x, int y, int w, int h) {
|
||||||
|
theme.drawGui(graphics, x, y, w, h, WidgetType.NORMAL);
|
||||||
|
// Title bar — darker strip across the top.
|
||||||
|
Color4I.rgba(0x80000000).draw(graphics, x + 2, y + 2, w - 4, 16);
|
||||||
|
theme.drawString(graphics, screenTitle, x + 8, y + 6, Color4I.WHITE, Theme.SHADOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Top y of the content area (below title bar). */
|
||||||
|
protected int contentTop() {
|
||||||
|
return getY() + 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bottom y of the content area (above action row). */
|
||||||
|
protected int contentBottom() {
|
||||||
|
return getY() + getHeight() - 24;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,196 @@
|
|||||||
package uk.sijbers.bnstoolkit.client.screens;
|
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.Theme;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.Widget;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.input.MouseButton;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
import uk.sijbers.bnstoolkit.client.ClientBnsState;
|
||||||
|
import uk.sijbers.bnstoolkit.data.Bounty;
|
||||||
|
import uk.sijbers.bnstoolkit.network.NetSend;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bounty board screen — replaces /bns bounty list / accept / turnin.
|
* Bounty board screen.
|
||||||
*
|
*
|
||||||
* Spec ref: docs/bnstoolkit-architecture.md §4.3 (BountyBoardScreen)
|
* Left: scrollable list of today's 5-bounty pool, plus any active bounties
|
||||||
|
* not currently in the pool (so you don't lose your in-progress hunt at
|
||||||
|
* day-rollover).
|
||||||
*
|
*
|
||||||
* TODO(matt M3): Layout to confirm tomorrow:
|
* Right: detail for the highlighted bounty — name, target entity, count
|
||||||
* - Left pane: list of 5 available bounties for today (refreshes every 24h
|
* needed, reward, current progress. Bottom row: ACCEPT / CANCEL /
|
||||||
* server-side; the screen just renders what the server sent)
|
* TURN IN buttons (enabled by state).
|
||||||
* - Right pane: detail for the highlighted bounty — target entity, count,
|
|
||||||
* payout, state (AVAILABLE / ACTIVE / READY / COOLDOWN)
|
|
||||||
* - Bottom row: ACCEPT / CANCEL / TURN IN buttons (enable based on state)
|
|
||||||
* - Slot indicator: "Active bounties: 2/5" using the player's tier cap
|
|
||||||
*
|
|
||||||
* Two C2S packets drive it:
|
|
||||||
* - RequestBountyList (board open / refresh)
|
|
||||||
* - RequestBountyAction (accept | cancel | turnin)
|
|
||||||
* One S2C: BountyBoardSnapshot.
|
|
||||||
*/
|
*/
|
||||||
public final class BountyBoardScreen extends BaseBnsScreen {
|
public final class BountyBoardScreen extends BaseBnsScreen {
|
||||||
|
private static String selected = null;
|
||||||
|
|
||||||
public BountyBoardScreen() {
|
public BountyBoardScreen() {
|
||||||
super(Component.translatable("screen.bnstoolkit.bounty_board"));
|
super(Component.translatable("screen.bnstoolkit.bounty_board"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(matt M3): init() + widgets
|
@Override
|
||||||
|
public void addWidgets() {
|
||||||
|
add(new BountyList(this));
|
||||||
|
add(new BountyDetail(this));
|
||||||
|
add(SimpleTextButton.create(this,
|
||||||
|
Component.literal("Refresh"),
|
||||||
|
Icons.REFRESH,
|
||||||
|
btn -> NetSend.requestSnapshot("BOUNTY")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void alignWidgets() {
|
||||||
|
getWidgets().get(0).setPos(getX() + 8, contentTop());
|
||||||
|
getWidgets().get(0).setSize(170, getHeight() - 30);
|
||||||
|
getWidgets().get(1).setPos(getX() + 186, contentTop());
|
||||||
|
getWidgets().get(1).setSize(getWidth() - 194, getHeight() - 30);
|
||||||
|
Widget rf = getWidgets().get(2);
|
||||||
|
rf.setSize(60, 16);
|
||||||
|
rf.setPos(getX() + getWidth() - 68, getY() + getHeight() - 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BountyList extends Panel {
|
||||||
|
BountyList(Panel parent) { super(parent); }
|
||||||
|
@Override public void addWidgets() {
|
||||||
|
// Today's daily pool first, then any active not already in pool
|
||||||
|
java.util.LinkedHashSet<String> show = new java.util.LinkedHashSet<>();
|
||||||
|
show.addAll(ClientBnsState.dailyPoolIds);
|
||||||
|
show.addAll(ClientBnsState.activeBounties.keySet());
|
||||||
|
int activeCount = ClientBnsState.activeBounties.size();
|
||||||
|
add(new HeaderRow(this, "Active: " + activeCount + " / " + ClientBnsState.bountySlotCap));
|
||||||
|
for (String id : show) {
|
||||||
|
Bounty b = Bounty.byId(id);
|
||||||
|
if (b != null) add(new BountyRow(this, b));
|
||||||
|
}
|
||||||
|
if (show.isEmpty()) {
|
||||||
|
add(new HeaderRow(this, "§7No bounties available — try Refresh"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public void alignWidgets() {
|
||||||
|
int y = 0;
|
||||||
|
for (Widget w : getWidgets()) {
|
||||||
|
w.setSize(width, 14);
|
||||||
|
w.setPos(0, y);
|
||||||
|
y += 14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class HeaderRow extends Widget {
|
||||||
|
private final String text;
|
||||||
|
HeaderRow(Panel parent, String text) { super(parent); this.text = text; }
|
||||||
|
@Override public void draw(GuiGraphics gfx, Theme theme, int x, int y, int w, int h) {
|
||||||
|
theme.drawString(gfx, Component.literal(text), x + 2, y + 3, Color4I.GRAY, Theme.SHADOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BountyRow extends Widget {
|
||||||
|
private final Bounty b;
|
||||||
|
BountyRow(Panel parent, Bounty b) { super(parent); this.b = b; }
|
||||||
|
@Override public void draw(GuiGraphics gfx, Theme theme, int x, int y, int w, int h) {
|
||||||
|
Bounty.State s = ClientBnsState.stateOf(b.id());
|
||||||
|
boolean sel = b.id().equals(selected);
|
||||||
|
if (sel) Color4I.rgba(0x80FFD700).draw(gfx, x, y, w, h);
|
||||||
|
else if (isMouseOver) Color4I.rgba(0x40FFFFFF).draw(gfx, x, y, w, h);
|
||||||
|
|
||||||
|
String prefix = switch (s) {
|
||||||
|
case ACTIVE -> "§e●§r ";
|
||||||
|
case READY -> "§a✓§r ";
|
||||||
|
case COOLDOWN -> "§8○§r ";
|
||||||
|
default -> "§7○§r ";
|
||||||
|
};
|
||||||
|
String suffix = switch (s) {
|
||||||
|
case ACTIVE -> {
|
||||||
|
ClientBnsState.ActiveBounty ab = ClientBnsState.activeBounties.get(b.id());
|
||||||
|
yield ab == null ? "" : " §7(" + ab.progress() + "/" + ab.target() + ")§r";
|
||||||
|
}
|
||||||
|
case READY -> " §aREADY§r";
|
||||||
|
case COOLDOWN -> " §8cooldown§r";
|
||||||
|
default -> " §6" + b.reward() + "§r";
|
||||||
|
};
|
||||||
|
theme.drawString(gfx, Component.literal(prefix + b.name() + suffix), x + 2, y + 3,
|
||||||
|
Color4I.WHITE, Theme.SHADOW);
|
||||||
|
}
|
||||||
|
@Override public boolean mousePressed(MouseButton button) {
|
||||||
|
if (isMouseOver) {
|
||||||
|
selected = b.id();
|
||||||
|
TierUpgradeScreen.playClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class BountyDetail extends Panel {
|
||||||
|
BountyDetail(Panel parent) { super(parent); }
|
||||||
|
|
||||||
|
@Override public void addWidgets() {
|
||||||
|
if (selected == null) {
|
||||||
|
add(new HeaderRow(this, "Select a bounty on the left."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bounty b = Bounty.byId(selected);
|
||||||
|
if (b == null) {
|
||||||
|
add(new HeaderRow(this, "§cUnknown bounty: " + selected));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Bounty.State s = ClientBnsState.stateOf(selected);
|
||||||
|
ClientBnsState.ActiveBounty ab = ClientBnsState.activeBounties.get(selected);
|
||||||
|
|
||||||
|
add(new HeaderRow(this, "§e" + b.name()));
|
||||||
|
add(new HeaderRow(this, "§7Target: " + b.targetEntity()));
|
||||||
|
add(new HeaderRow(this, "§7Count needed: " + b.count()));
|
||||||
|
add(new HeaderRow(this, "§6Reward: " + b.reward() + " spurs"));
|
||||||
|
add(new HeaderRow(this, "Status: " + statusColour(s)));
|
||||||
|
if (ab != null) add(new HeaderRow(this, "Progress: " + ab.progress() + " / " + ab.target()));
|
||||||
|
|
||||||
|
boolean atCap = ClientBnsState.activeBounties.size() >= ClientBnsState.bountySlotCap;
|
||||||
|
boolean isActive = s == Bounty.State.ACTIVE || s == Bounty.State.READY;
|
||||||
|
|
||||||
|
if (s == Bounty.State.AVAILABLE && !atCap) {
|
||||||
|
add(SimpleTextButton.create(this, Component.literal("Accept"), Icons.ACCEPT,
|
||||||
|
btn -> { NetSend.runBns("bounty accept " + b.id()); TierUpgradeScreen.playClick(); }));
|
||||||
|
}
|
||||||
|
if (s == Bounty.State.ACTIVE) {
|
||||||
|
add(SimpleTextButton.create(this, Component.literal("Cancel"), Icons.CANCEL,
|
||||||
|
btn -> { NetSend.runBns("bounty cancel " + b.id()); TierUpgradeScreen.playClick(); }));
|
||||||
|
}
|
||||||
|
if (s == Bounty.State.READY) {
|
||||||
|
add(SimpleTextButton.create(this, Component.literal("Turn In"), Icons.ACCEPT,
|
||||||
|
btn -> { NetSend.runBns("bounty turnin " + b.id()); TierUpgradeScreen.playClick(); }));
|
||||||
|
}
|
||||||
|
if (s == Bounty.State.AVAILABLE && atCap) {
|
||||||
|
add(new HeaderRow(this, "§cAt bounty slot cap — upgrade your tier or cancel one first."));
|
||||||
|
}
|
||||||
|
// ignore isActive flag silently; included for future buttons
|
||||||
|
if (isActive) { /* visual cue handled via row marker */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void alignWidgets() {
|
||||||
|
int y = 0;
|
||||||
|
for (Widget w : getWidgets()) {
|
||||||
|
if (w instanceof HeaderRow) {
|
||||||
|
w.setSize(width, 12);
|
||||||
|
w.setPos(0, y);
|
||||||
|
y += 12;
|
||||||
|
} else {
|
||||||
|
w.setSize(width, 18);
|
||||||
|
w.setPos(0, height - 18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String statusColour(Bounty.State s) {
|
||||||
|
return switch (s) {
|
||||||
|
case AVAILABLE -> "§7available";
|
||||||
|
case ACTIVE -> "§eactive";
|
||||||
|
case READY -> "§aready to turn in";
|
||||||
|
case COOLDOWN -> "§8on cooldown";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,170 @@
|
|||||||
package uk.sijbers.bnstoolkit.client.screens;
|
package uk.sijbers.bnstoolkit.client.screens;
|
||||||
|
|
||||||
|
import dev.ftb.mods.ftblibrary.icon.Color4I;
|
||||||
|
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.Theme;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.Widget;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.input.MouseButton;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
import uk.sijbers.bnstoolkit.client.ClientBnsState;
|
||||||
|
import uk.sijbers.bnstoolkit.data.Plot;
|
||||||
|
import uk.sijbers.bnstoolkit.network.NetSend;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Plot purchase + management screen — replaces /bns plot list / buy / release.
|
* Spawn-region plot purchase screen.
|
||||||
*
|
*
|
||||||
* Spec ref: docs/bnstoolkit-architecture.md §4.4 (PlotPurchaseScreen)
|
* Left: list of all plots in the spawn region (currently 3), state-coded
|
||||||
|
* by ownership. Right: detail for the selected plot — name, size, chunks,
|
||||||
|
* price, refund preview if owned. Bottom buttons: BUY or RELEASE.
|
||||||
*
|
*
|
||||||
* TODO(matt M3): Layout to confirm tomorrow:
|
* No minimap widget in M1 — the chunk list shows you exactly where each
|
||||||
* - Top-down minimap of the spawn plot region, plots coloured by state
|
* plot sits. A real map view (with bluemap thumbnail or live render) is
|
||||||
* (OWN / AVAILABLE / TAKEN_BY_OTHER), hover for owner + price
|
* planned in a follow-up.
|
||||||
* - Right pane: highlighted plot details — id, chunk bounds, price,
|
|
||||||
* owner, release refund preview
|
|
||||||
* - Bottom: BUY / RELEASE buttons (price + refund displayed inline)
|
|
||||||
* - Slot indicator: "Plots owned: 1/2" using the player's tier cap
|
|
||||||
*
|
|
||||||
* Two C2S packets:
|
|
||||||
* - RequestPlotMap (open / refresh)
|
|
||||||
* - RequestPlotAction (buy | release, with plot id)
|
|
||||||
* One S2C: PlotMapSnapshot.
|
|
||||||
*
|
|
||||||
* Open trigger options (decide tomorrow): keybind P, an in-game block at
|
|
||||||
* the spawn building, or both. Both is easy — they're not exclusive.
|
|
||||||
*/
|
*/
|
||||||
public final class PlotPurchaseScreen extends BaseBnsScreen {
|
public final class PlotPurchaseScreen extends BaseBnsScreen {
|
||||||
|
private static String selected = null;
|
||||||
|
|
||||||
public PlotPurchaseScreen() {
|
public PlotPurchaseScreen() {
|
||||||
super(Component.translatable("screen.bnstoolkit.plot_purchase"));
|
super(Component.translatable("screen.bnstoolkit.plot_purchase"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(matt M3): init() + map widget + action buttons
|
@Override
|
||||||
|
public void addWidgets() {
|
||||||
|
add(new PlotList(this));
|
||||||
|
add(new PlotDetail(this));
|
||||||
|
add(SimpleTextButton.create(this,
|
||||||
|
Component.literal("Refresh"),
|
||||||
|
Icons.REFRESH,
|
||||||
|
btn -> NetSend.requestSnapshot("PLOT")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void alignWidgets() {
|
||||||
|
getWidgets().get(0).setPos(getX() + 8, contentTop());
|
||||||
|
getWidgets().get(0).setSize(170, getHeight() - 30);
|
||||||
|
getWidgets().get(1).setPos(getX() + 186, contentTop());
|
||||||
|
getWidgets().get(1).setSize(getWidth() - 194, getHeight() - 30);
|
||||||
|
Widget rf = getWidgets().get(2);
|
||||||
|
rf.setSize(60, 16);
|
||||||
|
rf.setPos(getX() + getWidth() - 68, getY() + getHeight() - 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PlotList extends Panel {
|
||||||
|
PlotList(Panel parent) { super(parent); }
|
||||||
|
@Override public void addWidgets() {
|
||||||
|
int owned = ClientBnsState.ownedPlotIds.size();
|
||||||
|
add(new RowLabel(this, "§7Slots: " + owned + " / " + ClientBnsState.plotSlotCap));
|
||||||
|
for (Plot p : Plot.ALL) add(new PlotRow(this, p));
|
||||||
|
}
|
||||||
|
@Override public void alignWidgets() {
|
||||||
|
int y = 0;
|
||||||
|
for (Widget w : getWidgets()) {
|
||||||
|
w.setSize(width, 14);
|
||||||
|
w.setPos(0, y);
|
||||||
|
y += 14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PlotRow extends Widget {
|
||||||
|
private final Plot p;
|
||||||
|
PlotRow(Panel parent, Plot p) { super(parent); this.p = p; }
|
||||||
|
@Override public void draw(GuiGraphics gfx, Theme theme, int x, int y, int w, int h) {
|
||||||
|
boolean sel = p.id().equals(selected);
|
||||||
|
String owner = ClientBnsState.ownerOf(p.id());
|
||||||
|
boolean isMine = ClientBnsState.ownsPlot(p.id());
|
||||||
|
boolean taken = !owner.isEmpty() && !isMine;
|
||||||
|
|
||||||
|
if (sel) Color4I.rgba(0x80FFD700).draw(gfx, x, y, w, h);
|
||||||
|
else if (isMouseOver) Color4I.rgba(0x40FFFFFF).draw(gfx, x, y, w, h);
|
||||||
|
|
||||||
|
String state = isMine ? "§a★ yours§r" :
|
||||||
|
taken ? "§c● " + owner + "§r" :
|
||||||
|
"§7○ free§r";
|
||||||
|
String line = "§e" + p.name() + "§r §6(" + p.price() + ")§r " + state;
|
||||||
|
theme.drawString(gfx, Component.literal(line), x + 2, y + 3, Color4I.WHITE, Theme.SHADOW);
|
||||||
|
}
|
||||||
|
@Override public boolean mousePressed(MouseButton button) {
|
||||||
|
if (isMouseOver) {
|
||||||
|
selected = p.id();
|
||||||
|
TierUpgradeScreen.playClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RowLabel extends Widget {
|
||||||
|
private final String text;
|
||||||
|
RowLabel(Panel parent, String text) { super(parent); this.text = text; }
|
||||||
|
@Override public void draw(GuiGraphics gfx, Theme theme, int x, int y, int w, int h) {
|
||||||
|
theme.drawString(gfx, Component.literal(text), x + 2, y + 3, Color4I.GRAY, Theme.SHADOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class PlotDetail extends Panel {
|
||||||
|
PlotDetail(Panel parent) { super(parent); }
|
||||||
|
|
||||||
|
@Override public void addWidgets() {
|
||||||
|
if (selected == null) {
|
||||||
|
add(new RowLabel(this, "Select a plot on the left."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Plot p = Plot.byId(selected);
|
||||||
|
if (p == null) {
|
||||||
|
add(new RowLabel(this, "§cUnknown plot: " + selected));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String owner = ClientBnsState.ownerOf(p.id());
|
||||||
|
boolean isMine = ClientBnsState.ownsPlot(p.id());
|
||||||
|
boolean taken = !owner.isEmpty() && !isMine;
|
||||||
|
boolean atCap = ClientBnsState.ownedPlotIds.size() >= ClientBnsState.plotSlotCap;
|
||||||
|
|
||||||
|
add(new RowLabel(this, "§e" + p.name()));
|
||||||
|
add(new RowLabel(this, "§7Size: " + p.size()));
|
||||||
|
add(new RowLabel(this, "§7Dimension: " + p.dimension()));
|
||||||
|
add(new RowLabel(this, "§7Chunks: " + chunksToString(p)));
|
||||||
|
add(new RowLabel(this, "§6Price: " + p.price() + " spurs"));
|
||||||
|
if (isMine) {
|
||||||
|
add(new RowLabel(this, "§aOwned by you (release refund: " + p.refundOnRelease() + ")"));
|
||||||
|
add(SimpleTextButton.create(this, Component.literal("Release"), Icons.CANCEL,
|
||||||
|
btn -> { NetSend.runBns("plot release " + p.id()); TierUpgradeScreen.playClick(); }));
|
||||||
|
} else if (taken) {
|
||||||
|
add(new RowLabel(this, "§cTaken by " + owner));
|
||||||
|
} else if (atCap) {
|
||||||
|
add(new RowLabel(this, "§cAt plot-slot cap — upgrade your tier"));
|
||||||
|
} else if (ClientBnsState.plotSlotCap <= 0) {
|
||||||
|
add(new RowLabel(this, "§cYou need tier 3 (Citizen) for a plot"));
|
||||||
|
} else {
|
||||||
|
add(SimpleTextButton.create(this, Component.literal("Buy"), Icons.ACCEPT,
|
||||||
|
btn -> { NetSend.runBns("plot buy " + p.id()); TierUpgradeScreen.playClick(); }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void alignWidgets() {
|
||||||
|
int y = 0;
|
||||||
|
for (Widget w : getWidgets()) {
|
||||||
|
if (w instanceof RowLabel) {
|
||||||
|
w.setSize(width, 12);
|
||||||
|
w.setPos(0, y);
|
||||||
|
y += 12;
|
||||||
|
} else {
|
||||||
|
w.setSize(width, 18);
|
||||||
|
w.setPos(0, height - 18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String chunksToString(Plot p) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int[] cc : p.chunks()) {
|
||||||
|
if (sb.length() > 0) sb.append(", ");
|
||||||
|
sb.append('[').append(cc[0]).append(',').append(cc[1]).append(']');
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,114 @@
|
|||||||
package uk.sijbers.bnstoolkit.client.screens;
|
package uk.sijbers.bnstoolkit.client.screens;
|
||||||
|
|
||||||
|
import dev.ftb.mods.ftblibrary.icon.Color4I;
|
||||||
|
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.Theme;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.Widget;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
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.network.NetSend;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quest log — the "What am I doing right now?" pane.
|
* Read-only "what am I doing right now" overview.
|
||||||
*
|
*
|
||||||
* Spec ref: docs/bnstoolkit-architecture.md §4.2 (QuestLogScreen)
|
* Shows the player's tier + balance summary at the top, then three
|
||||||
*
|
* sections: active bounties with progress bars, owned plots, and the
|
||||||
* TODO(matt M2): Layout to confirm tomorrow:
|
* shop fee preview. FTB Quests integration deferred — when the quest
|
||||||
* - Tab bar at top: ACTIVE BOUNTIES | FTB QUESTS | PLOT TIMERS
|
* book lands we'll add a fourth section that mirrors the player's
|
||||||
* - Active Bounties tab: same data as Bounty Board's right pane but
|
* current chapter progress here.
|
||||||
* filtered to ACTIVE state, with kill-progress bars
|
|
||||||
* - FTB Quests tab: read-only mirror of the player's current chapter,
|
|
||||||
* showing in-progress quests + reward previews
|
|
||||||
* - Plot Timers tab: list of owned plots with any pending refund/release
|
|
||||||
* timers (when implemented)
|
|
||||||
*
|
|
||||||
* Refresh model: server pushes a QuestLogSnapshot whenever any tracked
|
|
||||||
* counter changes (kill count, quest reward, plot ownership). Screen re-
|
|
||||||
* renders from the latest snapshot.
|
|
||||||
*/
|
*/
|
||||||
public final class QuestLogScreen extends BaseBnsScreen {
|
public final class QuestLogScreen extends BaseBnsScreen {
|
||||||
public QuestLogScreen() {
|
public QuestLogScreen() {
|
||||||
super(Component.translatable("screen.bnstoolkit.quest_log"));
|
super(Component.translatable("screen.bnstoolkit.quest_log"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(matt M2): init() + tab widgets
|
@Override
|
||||||
|
public void addWidgets() {
|
||||||
|
add(new SummaryPanel(this));
|
||||||
|
add(SimpleTextButton.create(this, Component.literal("Refresh"), Icons.REFRESH,
|
||||||
|
btn -> NetSend.requestSnapshot("ALL")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void alignWidgets() {
|
||||||
|
getWidgets().get(0).setPos(getX() + 8, contentTop());
|
||||||
|
getWidgets().get(0).setSize(getWidth() - 16, getHeight() - 30);
|
||||||
|
Widget rf = getWidgets().get(1);
|
||||||
|
rf.setSize(60, 16);
|
||||||
|
rf.setPos(getX() + getWidth() - 68, getY() + getHeight() - 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class SummaryPanel extends Panel {
|
||||||
|
SummaryPanel(Panel parent) { super(parent); }
|
||||||
|
|
||||||
|
@Override public void addWidgets() {
|
||||||
|
CivicTier cfg = CivicTier.byId(ClientBnsState.tier);
|
||||||
|
add(new Header(this, "§e=== Status ==="));
|
||||||
|
add(new Header(this, "Tier: §" + cfg.colour().getChar() + cfg.name() + "§r ("
|
||||||
|
+ ClientBnsState.tier + " / " + CivicTier.MAX + ")"));
|
||||||
|
add(new Header(this, "Balance: §6" + TierUpgradeScreen.formatSpurs(ClientBnsState.spurs) + " spurs§r"));
|
||||||
|
add(new Header(this, "Shop fee: §c" + ClientBnsState.effectiveFeePct + "%§r"));
|
||||||
|
|
||||||
|
add(new Header(this, ""));
|
||||||
|
add(new Header(this, "§e=== Active bounties ==="));
|
||||||
|
if (ClientBnsState.activeBounties.isEmpty()) {
|
||||||
|
add(new Header(this, "§7none — visit the Bounty Board (B)"));
|
||||||
|
} else {
|
||||||
|
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(), 14);
|
||||||
|
String line = " " + b.name() + " " + bar + " §7" + ab.progress() + "/" + ab.target() + "§r";
|
||||||
|
add(new Header(this, line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(new Header(this, ""));
|
||||||
|
add(new Header(this, "§e=== Owned plots ==="));
|
||||||
|
if (ClientBnsState.ownedPlotIds.isEmpty()) {
|
||||||
|
add(new Header(this, "§7none — visit the Plot Office (P)"));
|
||||||
|
} 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void alignWidgets() {
|
||||||
|
int y = 0;
|
||||||
|
for (Widget w : getWidgets()) {
|
||||||
|
w.setSize(width, 12);
|
||||||
|
w.setPos(0, y);
|
||||||
|
y += 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String progressBar(int progress, int target, int barWidth) {
|
||||||
|
int filled = target > 0 ? Math.min(barWidth, (progress * barWidth) / target) : 0;
|
||||||
|
StringBuilder sb = new StringBuilder("§a");
|
||||||
|
for (int i = 0; i < filled; i++) sb.append('█');
|
||||||
|
sb.append("§8");
|
||||||
|
for (int i = filled; i < barWidth; i++) sb.append('▒');
|
||||||
|
sb.append("§r");
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Header extends Widget {
|
||||||
|
private final String text;
|
||||||
|
Header(Panel parent, String text) { super(parent); this.text = text; }
|
||||||
|
@Override public void draw(GuiGraphics gfx, Theme theme, int x, int y, int w, int h) {
|
||||||
|
theme.drawString(gfx, Component.literal(text), x, y + 2, Color4I.WHITE, Theme.SHADOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,172 @@
|
|||||||
package uk.sijbers.bnstoolkit.client.screens;
|
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.icon.ItemIcon;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.Panel;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.SimpleTextButton;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.Theme;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.Widget;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.input.MouseButton;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.item.Item;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import uk.sijbers.bnstoolkit.client.ClientBnsState;
|
||||||
|
import uk.sijbers.bnstoolkit.data.SellCatalogue;
|
||||||
|
import uk.sijbers.bnstoolkit.network.NetSend;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bazaar shop browser — replaces /bns sell list / sell <item> [count].
|
* Bazaar sell-shop screen.
|
||||||
*
|
*
|
||||||
* Spec ref: docs/bnstoolkit-architecture.md §4.5 (ShopBrowserScreen)
|
* Grid of 9 sell-list items. Click an item to select it. The right pane
|
||||||
|
* shows the selected item's effective price after fee + diminishing
|
||||||
|
* returns. Three quick-sell buttons: 1 / 16 / 64 (or whatever fits in
|
||||||
|
* the player's inventory).
|
||||||
*
|
*
|
||||||
* TODO(matt M2): Layout to confirm tomorrow:
|
* Sell action: sends a "sell <item> <count>" routed C2S packet. Server
|
||||||
* - Grid of sell-list items (9 in the current catalogue)
|
* runs the KubeJS-owned /bns sell verb; KubeJS subtracts items and pays
|
||||||
* - Per cell: item icon, base price, effective price after DR + tier fee,
|
* the spurs; server pushes both an updated SellSnapshot AND a fresh
|
||||||
* "you've sold X" counter
|
* tier/balance.
|
||||||
* - Click cell -> count slider (1 / 16 / 64 / max-in-inventory) and SELL button
|
|
||||||
* - Header: current spurs + effective fee%
|
|
||||||
* - Future: BUY tab when buy-prices land (Phase 7 dynamic exchange)
|
|
||||||
*
|
|
||||||
* Two C2S packets:
|
|
||||||
* - RequestShopCatalogue (open / refresh)
|
|
||||||
* - RequestShopSell (item id, count)
|
|
||||||
* One S2C: ShopCatalogueSnapshot, plus per-sale ShopSellResult.
|
|
||||||
*
|
|
||||||
* Open trigger options: keybind, a Numismatics-style shop block at the
|
|
||||||
* Bazaar building, or both.
|
|
||||||
*/
|
*/
|
||||||
public final class ShopBrowserScreen extends BaseBnsScreen {
|
public final class ShopBrowserScreen extends BaseBnsScreen {
|
||||||
public ShopBrowserScreen() {
|
public ShopBrowserScreen() {
|
||||||
super(Component.translatable("screen.bnstoolkit.shop_browser"));
|
super(Component.translatable("screen.bnstoolkit.shop_browser"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(matt M2): init() + item grid + sell widget
|
private static ResourceLocation selected = ResourceLocation.parse("minecraft:diamond");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addWidgets() {
|
||||||
|
add(new ItemGrid(this));
|
||||||
|
add(new SellDetail(this));
|
||||||
|
add(SimpleTextButton.create(this,
|
||||||
|
Component.literal("Refresh"),
|
||||||
|
Icons.REFRESH,
|
||||||
|
btn -> NetSend.requestSnapshot("SELL")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void alignWidgets() {
|
||||||
|
getWidgets().get(0).setPos(getX() + 8, contentTop());
|
||||||
|
getWidgets().get(0).setSize(170, getHeight() - 30);
|
||||||
|
getWidgets().get(1).setPos(getX() + 186, contentTop());
|
||||||
|
getWidgets().get(1).setSize(getWidth() - 194, getHeight() - 30);
|
||||||
|
Widget rf = getWidgets().get(2);
|
||||||
|
rf.setSize(60, 16);
|
||||||
|
rf.setPos(getX() + getWidth() - 68, getY() + getHeight() - 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Item grid ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static final class ItemGrid extends Panel {
|
||||||
|
ItemGrid(Panel parent) { super(parent); }
|
||||||
|
@Override public void addWidgets() {
|
||||||
|
for (ResourceLocation id : SellCatalogue.PRICES.keySet()) add(new ItemCell(this, id));
|
||||||
|
}
|
||||||
|
@Override public void alignWidgets() {
|
||||||
|
int cols = 5, cell = 32, gap = 2;
|
||||||
|
int i = 0;
|
||||||
|
for (Widget w : getWidgets()) {
|
||||||
|
int col = i % cols;
|
||||||
|
int row = i / cols;
|
||||||
|
w.setSize(cell, cell);
|
||||||
|
w.setPos(col * (cell + gap), row * (cell + gap));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ItemCell extends Widget {
|
||||||
|
private final ResourceLocation itemId;
|
||||||
|
private final ItemStack stack;
|
||||||
|
ItemCell(Panel parent, ResourceLocation itemId) {
|
||||||
|
super(parent);
|
||||||
|
this.itemId = itemId;
|
||||||
|
Item item = BuiltInRegistries.ITEM.get(itemId);
|
||||||
|
this.stack = item == null ? ItemStack.EMPTY : new ItemStack(item);
|
||||||
|
}
|
||||||
|
@Override public void draw(GuiGraphics gfx, Theme theme, int x, int y, int w, int h) {
|
||||||
|
boolean isSelected = itemId.equals(selected);
|
||||||
|
Color4I bg = isSelected ? Color4I.rgba(0x80FFD700) :
|
||||||
|
isMouseOver ? Color4I.rgba(0x40FFFFFF) : Color4I.rgba(0x40000000);
|
||||||
|
bg.draw(gfx, x, y, w, h);
|
||||||
|
theme.drawString(gfx, Component.empty(), x, y, Color4I.WHITE, 0);
|
||||||
|
// Render the item icon centered
|
||||||
|
if (!stack.isEmpty()) {
|
||||||
|
gfx.renderItem(stack, x + 8, y + 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Override public boolean mousePressed(MouseButton button) {
|
||||||
|
if (isMouseOver) {
|
||||||
|
selected = itemId;
|
||||||
|
TierUpgradeScreen.playClick();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Detail pane ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static final class SellDetail extends Panel {
|
||||||
|
SellDetail(Panel parent) { super(parent); }
|
||||||
|
|
||||||
|
@Override public void addWidgets() {
|
||||||
|
long base = SellCatalogue.PRICES.getOrDefault(selected, 0L);
|
||||||
|
long sold = ClientBnsState.lifetimeSoldOf(selected);
|
||||||
|
int feePct = ClientBnsState.effectiveFeePct;
|
||||||
|
long perUnit = SellCatalogue.singleUnitPriceDisplay(selected, sold, feePct);
|
||||||
|
|
||||||
|
add(new LabelRow(this, "§e" + selected.toString() + "§r"));
|
||||||
|
add(new LabelRow(this, ""));
|
||||||
|
add(new LabelRow(this, "List price: §6" + base + "§r"));
|
||||||
|
add(new LabelRow(this, "Sold lifetime: §7" + sold + "§r"));
|
||||||
|
add(new LabelRow(this, "Effective fee: §c" + feePct + "%§r"));
|
||||||
|
add(new LabelRow(this, "You get: §a" + perUnit + " spurs / unit§r"));
|
||||||
|
add(new LabelRow(this, ""));
|
||||||
|
|
||||||
|
for (int n : new int[]{1, 16, 64}) {
|
||||||
|
final int count = n;
|
||||||
|
add(SimpleTextButton.create(this,
|
||||||
|
Component.literal("Sell " + count),
|
||||||
|
Icon.empty(),
|
||||||
|
btn -> {
|
||||||
|
NetSend.runBns("sell " + selected + " " + count);
|
||||||
|
TierUpgradeScreen.playClick();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void alignWidgets() {
|
||||||
|
int yCursor = 0;
|
||||||
|
int btnY = height - 20;
|
||||||
|
int btnX = 0;
|
||||||
|
for (Widget w : getWidgets()) {
|
||||||
|
if (w instanceof LabelRow) {
|
||||||
|
w.setSize(width, 12);
|
||||||
|
w.setPos(0, yCursor);
|
||||||
|
yCursor += 12;
|
||||||
|
} else {
|
||||||
|
int bw = (width - 4) / 3;
|
||||||
|
w.setSize(bw, 18);
|
||||||
|
w.setPos(btnX, btnY);
|
||||||
|
btnX += bw + 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class LabelRow extends Widget {
|
||||||
|
private final String text;
|
||||||
|
LabelRow(Panel parent, String text) { super(parent); this.text = text; }
|
||||||
|
@Override public void draw(GuiGraphics gfx, Theme theme, int x, int y, int w, int h) {
|
||||||
|
theme.drawString(gfx, Component.literal(text), x, y + 2, Color4I.WHITE, Theme.SHADOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,178 @@
|
|||||||
package uk.sijbers.bnstoolkit.client.screens;
|
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.Theme;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.Widget;
|
||||||
|
import dev.ftb.mods.ftblibrary.ui.WidgetLayout;
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.client.resources.sounds.SimpleSoundInstance;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.network.chat.Component;
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.sounds.SoundEvents;
|
||||||
|
import uk.sijbers.bnstoolkit.client.ClientBnsState;
|
||||||
|
import uk.sijbers.bnstoolkit.data.CivicTier;
|
||||||
|
import uk.sijbers.bnstoolkit.network.NetSend;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tier ladder + upgrade screen.
|
* Civic tier ladder + upgrade button.
|
||||||
*
|
*
|
||||||
* Spec ref: docs/bnstoolkit-architecture.md §4.1 (TierUpgradeScreen)
|
* Left side: 13 rows for the tiers. Current tier row gets a highlighted
|
||||||
|
* background and a "▶" marker. Each row shows: tier number, name in its
|
||||||
|
* rank colour, cost in spurs, summary perks.
|
||||||
*
|
*
|
||||||
* TODO(matt M1): Visual layout to confirm with you tomorrow:
|
* Right side: detail card for the *next* tier (or current if you're at
|
||||||
* - 13 rows, one per civic tier (Peasant -> Sovereign)
|
* the top) plus the Upgrade button. Button is disabled when you can't
|
||||||
* - Current tier row highlighted
|
* afford it or you're at Sovereign.
|
||||||
* - Each row: rank colour name, cost in spurs, perks summary
|
|
||||||
* (chunks / plot-slots / bounty-slots / shop-fee)
|
|
||||||
* - "Upgrade to <next>" button bottom-right (only enabled if affordable)
|
|
||||||
* - Click pre-confirms cost + shows current balance, second click commits
|
|
||||||
*
|
|
||||||
* Backend wiring: button click sends a C2S RequestTierUpgrade packet.
|
|
||||||
* Server replies with TierUpgradeResult (success/failure + new tier + new
|
|
||||||
* balance). Same backend the /bns tier upgrade chat command already uses.
|
|
||||||
*/
|
*/
|
||||||
public final class TierUpgradeScreen extends BaseBnsScreen {
|
public final class TierUpgradeScreen extends BaseBnsScreen {
|
||||||
|
|
||||||
public TierUpgradeScreen() {
|
public TierUpgradeScreen() {
|
||||||
super(Component.translatable("screen.bnstoolkit.tier_upgrade"));
|
super(Component.translatable("screen.bnstoolkit.tier_upgrade"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(matt M1): override init() to lay out rows + button widgets.
|
@Override
|
||||||
|
public void addWidgets() {
|
||||||
|
add(new LadderPanel(this));
|
||||||
|
add(new DetailPanel(this));
|
||||||
|
add(SimpleTextButton.create(this,
|
||||||
|
Component.literal("Refresh"),
|
||||||
|
Icons.REFRESH,
|
||||||
|
btn -> NetSend.requestSnapshot("TIER")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void alignWidgets() {
|
||||||
|
// ladder panel
|
||||||
|
getWidgets().get(0).setPos(getX() + 8, contentTop());
|
||||||
|
getWidgets().get(0).setSize(180, getHeight() - 28);
|
||||||
|
// detail panel
|
||||||
|
getWidgets().get(1).setPos(getX() + 196, contentTop());
|
||||||
|
getWidgets().get(1).setSize(getWidth() - 204, getHeight() - 50);
|
||||||
|
// refresh button bottom-right
|
||||||
|
Widget rf = getWidgets().get(2);
|
||||||
|
rf.setSize(60, 16);
|
||||||
|
rf.setPos(getX() + getWidth() - 68, getY() + getHeight() - 22);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sub-panels ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static final class LadderPanel extends Panel {
|
||||||
|
LadderPanel(Panel parent) { super(parent); }
|
||||||
|
@Override public void addWidgets() {
|
||||||
|
for (CivicTier t : CivicTier.LADDER) add(new LadderRow(this, t));
|
||||||
|
}
|
||||||
|
@Override public void alignWidgets() {
|
||||||
|
WidgetLayout.VERTICAL.align(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class LadderRow extends Widget {
|
||||||
|
private final CivicTier cfg;
|
||||||
|
LadderRow(Panel parent, CivicTier cfg) {
|
||||||
|
super(parent);
|
||||||
|
this.cfg = cfg;
|
||||||
|
setSize(parent.width, 13);
|
||||||
|
}
|
||||||
|
@Override public void draw(GuiGraphics gfx, Theme theme, int x, int y, int w, int h) {
|
||||||
|
boolean current = ClientBnsState.tier == cfg.id();
|
||||||
|
if (current) {
|
||||||
|
Color4I.rgba(0x80FFD700).draw(gfx, x, y, w, h);
|
||||||
|
} else if (isMouseOver) {
|
||||||
|
Color4I.rgba(0x40FFFFFF).draw(gfx, x, y, w, h);
|
||||||
|
}
|
||||||
|
String marker = current ? "▶" : " ";
|
||||||
|
String line = String.format("%s %2d. %s§r §7%s spurs",
|
||||||
|
marker, cfg.id(),
|
||||||
|
"§" + colourCode(cfg.colour()) + cfg.name(),
|
||||||
|
formatSpurs(cfg.cost()));
|
||||||
|
theme.drawString(gfx, Component.literal(line), x + 4, y + 2,
|
||||||
|
current ? Color4I.WHITE : Color4I.GRAY, Theme.SHADOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class DetailPanel extends Panel {
|
||||||
|
DetailPanel(Panel parent) { super(parent); }
|
||||||
|
|
||||||
|
@Override public void addWidgets() {
|
||||||
|
int currentTier = ClientBnsState.tier;
|
||||||
|
boolean atMax = currentTier >= CivicTier.MAX;
|
||||||
|
if (atMax) {
|
||||||
|
add(new InfoLabel(this, "You have reached the Sovereign tier."));
|
||||||
|
add(new InfoLabel(this, "Nothing more to ascend to."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CivicTier next = CivicTier.byId(currentTier + 1);
|
||||||
|
long cost = next.cost();
|
||||||
|
boolean canAfford = ClientBnsState.spurs >= cost;
|
||||||
|
|
||||||
|
add(new InfoLabel(this, String.format("Next: §%c%s§r", colourCode(next.colour()), next.name())));
|
||||||
|
add(new InfoLabel(this, "Cost: §6" + formatSpurs(cost) + " spurs§r"));
|
||||||
|
add(new InfoLabel(this, "You have: " + (canAfford ? "§a" : "§c") + formatSpurs(ClientBnsState.spurs) + "§r"));
|
||||||
|
add(new InfoLabel(this, ""));
|
||||||
|
add(new InfoLabel(this, "Perks at " + next.name() + ":"));
|
||||||
|
add(new InfoLabel(this, " • Chunks: " + next.wildernessChunks()));
|
||||||
|
add(new InfoLabel(this, " • Plot slots: " + next.plotSlots()));
|
||||||
|
add(new InfoLabel(this, " • Daily bounties: " + next.dailyBountySlots()));
|
||||||
|
add(new InfoLabel(this, " • Shop fee: " + next.effectiveShopFeePct() + "%"));
|
||||||
|
|
||||||
|
SimpleTextButton upgrade = SimpleTextButton.create(this,
|
||||||
|
Component.literal("Upgrade to " + next.name()),
|
||||||
|
Icon.empty(),
|
||||||
|
btn -> {
|
||||||
|
if (!canAfford) {
|
||||||
|
playClick();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NetSend.runBns("tier upgrade");
|
||||||
|
playClick();
|
||||||
|
});
|
||||||
|
add(upgrade);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void alignWidgets() {
|
||||||
|
int y = 0;
|
||||||
|
for (Widget w : getWidgets()) {
|
||||||
|
if (w instanceof InfoLabel) {
|
||||||
|
w.setSize(width, 12);
|
||||||
|
w.setPos(0, y);
|
||||||
|
y += 12;
|
||||||
|
} else {
|
||||||
|
w.setSize(width, 18);
|
||||||
|
w.setPos(0, height - 18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class InfoLabel extends Widget {
|
||||||
|
private final String text;
|
||||||
|
InfoLabel(Panel parent, String text) { super(parent); this.text = text; }
|
||||||
|
@Override public void draw(GuiGraphics gfx, Theme theme, int x, int y, int w, int h) {
|
||||||
|
theme.drawString(gfx, Component.literal(text), x, y + 2, Color4I.WHITE, Theme.SHADOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── helpers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static char colourCode(ChatFormatting c) {
|
||||||
|
return c.getChar();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String formatSpurs(long n) {
|
||||||
|
if (n >= 1_000_000) return String.format("%.1fM", n / 1_000_000.0);
|
||||||
|
if (n >= 10_000) return String.format("%dk", n / 1000);
|
||||||
|
if (n >= 1_000) return String.format("%.1fk", n / 1000.0);
|
||||||
|
return Long.toString(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void playClick() {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc != null) mc.getSoundManager().play(
|
||||||
|
SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK.value(), 1.0F));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bounty catalogue. Mirrors BOUNTY_POOL in
|
||||||
|
* pack/overrides/kubejs/server_scripts/economy/03_bounty_engine.js.
|
||||||
|
*
|
||||||
|
* Static — the pool of 20 doesn't change at runtime. Per-player daily
|
||||||
|
* selection (5 of these), active set, completion state all live in the
|
||||||
|
* player's persistentData NBT and arrive via the BountyBoardSnapshot
|
||||||
|
* S2C packet.
|
||||||
|
*/
|
||||||
|
public record Bounty(String id, String name, String targetEntity, int count, long reward, String source) {
|
||||||
|
|
||||||
|
public static final List<Bounty> POOL = List.of(
|
||||||
|
new Bounty("b_zombie_10", "Slay 10 Zombies", "minecraft:zombie", 10, 15L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_skeleton_10", "Slay 10 Skeletons", "minecraft:skeleton", 10, 15L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_spider_8", "Slay 8 Spiders", "minecraft:spider", 8, 12L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_creeper_5", "Slay 5 Creepers", "minecraft:creeper", 5, 20L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_drowned_8", "Slay 8 Drowned", "minecraft:drowned", 8, 18L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_husk_8", "Slay 8 Husks", "minecraft:husk", 8, 18L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_blaze_5", "Slay 5 Blazes", "minecraft:blaze", 5, 50L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_piglin_8", "Slay 8 Piglins", "minecraft:piglin", 8, 40L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_magma_6", "Slay 6 Magma Cubes", "minecraft:magma_cube", 6, 30L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_ghast_3", "Slay 3 Ghasts", "minecraft:ghast", 3, 60L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_plunderer_5", "Hunt 5 Plunderers", "supplementaries:plunderer", 5, 75L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_witch_3", "Slay 3 Witches", "minecraft:witch", 3, 80L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_enderman_5", "Slay 5 Endermen", "minecraft:enderman", 5, 90L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_wither_skel_3", "Slay 3 Wither Skeletons","minecraft:wither_skeleton", 3, 150L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_shulker_3", "Slay 3 Shulkers", "minecraft:shulker", 3, 200L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_warden_1", "Slay the Warden", "minecraft:warden", 1, 800L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_voidworm_1", "Destroy a Void Worm", "alexsmobs:void_worm", 1, 1200L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_warped_mosco_1", "Slay a Warped Mosco", "alexsmobs:warped_mosco", 1, 700L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_farseer_1", "Slay the Farseer", "alexsmobs:farseer", 1, 900L, "Adventurer's Guild"),
|
||||||
|
new Bounty("b_skreecher_1", "Slay a Skreecher", "alexsmobs:skreecher", 1, 400L, "Adventurer's Guild")
|
||||||
|
);
|
||||||
|
|
||||||
|
public static Bounty byId(String id) {
|
||||||
|
for (Bounty b : POOL) if (b.id.equals(id)) return b;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum State { AVAILABLE, ACTIVE, READY, COOLDOWN }
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.data;
|
||||||
|
|
||||||
|
import net.minecraft.ChatFormatting;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 13-tier civic ladder. Mirrors TIER_CONFIG in
|
||||||
|
* pack/overrides/kubejs/server_scripts/economy/00_tier_system.js.
|
||||||
|
*
|
||||||
|
* Source of truth is still the KubeJS table — this Java copy exists so the
|
||||||
|
* client-side screens can render the ladder without round-tripping every
|
||||||
|
* row to the server. The two must be kept in sync; if a value changes
|
||||||
|
* here, change it there too.
|
||||||
|
*/
|
||||||
|
public record CivicTier(
|
||||||
|
int id,
|
||||||
|
String name,
|
||||||
|
long cost,
|
||||||
|
int wildernessChunks,
|
||||||
|
int plotSlots,
|
||||||
|
int dailyBountySlots,
|
||||||
|
int shopFeeDiscount,
|
||||||
|
ChatFormatting colour) {
|
||||||
|
|
||||||
|
public static final List<CivicTier> LADDER = List.of(
|
||||||
|
new CivicTier( 1, "Peasant", 0L, 9, 0, 3, 0, ChatFormatting.GRAY),
|
||||||
|
new CivicTier( 2, "Farmer", 200L, 18, 0, 3, 0, ChatFormatting.GRAY),
|
||||||
|
new CivicTier( 3, "Citizen", 750L, 35, 1, 3, 0, ChatFormatting.WHITE),
|
||||||
|
new CivicTier( 4, "Merchant", 2500L, 60, 1, 4, -2, ChatFormatting.YELLOW),
|
||||||
|
new CivicTier( 5, "Knight", 6500L, 100, 2, 4, -5, ChatFormatting.GREEN),
|
||||||
|
new CivicTier( 6, "Baron", 15000L, 150, 2, 5, -7, ChatFormatting.AQUA),
|
||||||
|
new CivicTier( 7, "Viscount", 35000L, 220, 3, 5, -9, ChatFormatting.BLUE),
|
||||||
|
new CivicTier( 8, "Earl", 75000L, 300, 3, 6, -11, ChatFormatting.DARK_PURPLE),
|
||||||
|
new CivicTier( 9, "Marquess", 150000L, 400, 4, 6, -13, ChatFormatting.GOLD),
|
||||||
|
new CivicTier(10, "Duke", 300000L, 525, 5, 7, -15, ChatFormatting.GOLD),
|
||||||
|
new CivicTier(11, "Archduke", 700000L, 700, 6, 7, -17, ChatFormatting.RED),
|
||||||
|
new CivicTier(12, "Grand Duke", 1500000L, 900, 7, 8, -19, ChatFormatting.RED),
|
||||||
|
new CivicTier(13, "Sovereign", 10000000L, 1500, 10, 10, -25, ChatFormatting.LIGHT_PURPLE)
|
||||||
|
);
|
||||||
|
|
||||||
|
public static final int MAX = 13;
|
||||||
|
public static final int BAZAAR_BASE_FEE_PCT = 25;
|
||||||
|
|
||||||
|
public static CivicTier byId(int id) {
|
||||||
|
if (id < 1 || id > MAX) return LADDER.get(0);
|
||||||
|
return LADDER.get(id - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int effectiveShopFeePct() {
|
||||||
|
return Math.max(0, BAZAAR_BASE_FEE_PCT + shopFeeDiscount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn-region plot catalogue. Mirrors PLOTS in
|
||||||
|
* pack/overrides/kubejs/server_scripts/plots.js.
|
||||||
|
*/
|
||||||
|
public record Plot(String id, String name, String size, long price, String dimension, List<int[]> chunks) {
|
||||||
|
|
||||||
|
public static final List<Plot> ALL = List.of(
|
||||||
|
new Plot("p001", "Market Stall 1", "small", 100L, "minecraft:overworld",
|
||||||
|
List.of(new int[]{10, 10})),
|
||||||
|
new Plot("p002", "Corner Lot", "medium", 250L, "minecraft:overworld",
|
||||||
|
List.of(new int[]{12, 10}, new int[]{12, 11})),
|
||||||
|
new Plot("p003", "Anchor Plot", "large", 600L, "minecraft:overworld",
|
||||||
|
List.of(new int[]{14, 10}, new int[]{14, 11}, new int[]{15, 10}, new int[]{15, 11}))
|
||||||
|
);
|
||||||
|
|
||||||
|
public static Plot byId(String id) {
|
||||||
|
for (Plot p : ALL) if (p.id.equals(id)) return p;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long refundOnRelease() {
|
||||||
|
return price / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.data;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bazaar sell prices. Mirrors SELL_PRICES in
|
||||||
|
* pack/overrides/kubejs/server_scripts/economy/02_sell_shop.js.
|
||||||
|
*
|
||||||
|
* The map is insertion-ordered so the screen grid matches the KubeJS
|
||||||
|
* config order without separate display ordering.
|
||||||
|
*/
|
||||||
|
public final class SellCatalogue {
|
||||||
|
private SellCatalogue() {}
|
||||||
|
|
||||||
|
public static final int DR_STEP = 64;
|
||||||
|
public static final double DR_DECAY = 0.10;
|
||||||
|
public static final double DR_FLOOR = 0.50;
|
||||||
|
|
||||||
|
public static final Map<ResourceLocation, Long> PRICES;
|
||||||
|
static {
|
||||||
|
Map<ResourceLocation, Long> m = new LinkedHashMap<>();
|
||||||
|
m.put(ResourceLocation.parse("minecraft:diamond"), 10L);
|
||||||
|
m.put(ResourceLocation.parse("minecraft:netherite_ingot"), 100L);
|
||||||
|
m.put(ResourceLocation.parse("minecraft:emerald"), 1L);
|
||||||
|
m.put(ResourceLocation.parse("minecraft:gold_ingot"), 2L);
|
||||||
|
m.put(ResourceLocation.parse("minecraft:iron_ingot"), 1L);
|
||||||
|
m.put(ResourceLocation.parse("minecraft:lapis_lazuli"), 1L);
|
||||||
|
m.put(ResourceLocation.parse("minecraft:redstone"), 1L);
|
||||||
|
m.put(ResourceLocation.parse("minecraft:coal"), 1L);
|
||||||
|
m.put(ResourceLocation.parse("minecraft:ancient_debris"), 500L);
|
||||||
|
PRICES = java.util.Collections.unmodifiableMap(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effective per-unit payout matching the KubeJS formula:
|
||||||
|
* midSteps = (sold + count/2) / DR_STEP
|
||||||
|
* drMult = max(DR_FLOOR, 1 - midSteps * DR_DECAY)
|
||||||
|
* payout = floor(base * drMult * (1 - feePct/100) * count)
|
||||||
|
*/
|
||||||
|
public static long effectivePayout(ResourceLocation item, long lifetimeSold, long count, int feePct) {
|
||||||
|
Long base = PRICES.get(item);
|
||||||
|
if (base == null) return 0L;
|
||||||
|
long midSteps = (lifetimeSold + count / 2L) / DR_STEP;
|
||||||
|
double drMult = Math.max(DR_FLOOR, 1.0 - midSteps * DR_DECAY);
|
||||||
|
double net = base * drMult * (1.0 - feePct / 100.0);
|
||||||
|
return (long) Math.floor(net * count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Display-only single-unit price for a player at their current sold count + fee. */
|
||||||
|
public static long singleUnitPriceDisplay(ResourceLocation item, long lifetimeSold, int feePct) {
|
||||||
|
Long base = PRICES.get(item);
|
||||||
|
if (base == null) return 0L;
|
||||||
|
long steps = lifetimeSold / DR_STEP;
|
||||||
|
double drMult = Math.max(DR_FLOOR, 1.0 - steps * DR_DECAY);
|
||||||
|
return (long) Math.floor(base * drMult * (1.0 - feePct / 100.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.data;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sums a player's inventory into a "spurs" balance using the Numismatics
|
||||||
|
* coin denomination table.
|
||||||
|
*
|
||||||
|
* Denominations match the Numismatics mod (1 spur = 1 base unit):
|
||||||
|
* spur = 1, bevel = 8, sprocket = 16, cog = 64, crown = 512, sun = 4096
|
||||||
|
*
|
||||||
|
* Mirrors the COIN_VALUES table in pack/overrides/kubejs/server_scripts/plots.js.
|
||||||
|
* Lives here in Java so the HUD can render the balance every frame without
|
||||||
|
* touching the server.
|
||||||
|
*/
|
||||||
|
public final class SpurBalance {
|
||||||
|
private SpurBalance() {}
|
||||||
|
|
||||||
|
public static final Map<ResourceLocation, Long> COIN_VALUES;
|
||||||
|
static {
|
||||||
|
Map<ResourceLocation, Long> m = new LinkedHashMap<>();
|
||||||
|
m.put(ResourceLocation.parse("numismatics:spur"), 1L);
|
||||||
|
m.put(ResourceLocation.parse("numismatics:bevel"), 8L);
|
||||||
|
m.put(ResourceLocation.parse("numismatics:sprocket"), 16L);
|
||||||
|
m.put(ResourceLocation.parse("numismatics:cog"), 64L);
|
||||||
|
m.put(ResourceLocation.parse("numismatics:crown"), 512L);
|
||||||
|
m.put(ResourceLocation.parse("numismatics:sun"), 4096L);
|
||||||
|
COIN_VALUES = java.util.Collections.unmodifiableMap(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Inventory-only scan (excludes ender chests, curios, backpacks). */
|
||||||
|
public static long totalSpurs(Player player) {
|
||||||
|
Inventory inv = player.getInventory();
|
||||||
|
long total = 0L;
|
||||||
|
for (int i = 0; i < inv.getContainerSize(); i++) {
|
||||||
|
ItemStack stack = inv.getItem(i);
|
||||||
|
if (stack.isEmpty()) continue;
|
||||||
|
ResourceLocation id = stack.getItemHolder().unwrapKey().map(k -> k.location()).orElse(null);
|
||||||
|
if (id == null) continue;
|
||||||
|
Long value = COIN_VALUES.get(id);
|
||||||
|
if (value == null) continue;
|
||||||
|
total += (long) stack.getCount() * value;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Count of a specific raw item id in the player's inventory (for sell screen prechecks). */
|
||||||
|
public static int countItem(Player player, ResourceLocation itemId) {
|
||||||
|
Inventory inv = player.getInventory();
|
||||||
|
int total = 0;
|
||||||
|
for (int i = 0; i < inv.getContainerSize(); i++) {
|
||||||
|
ItemStack stack = inv.getItem(i);
|
||||||
|
if (stack.isEmpty()) continue;
|
||||||
|
ResourceLocation id = stack.getItemHolder().unwrapKey().map(k -> k.location()).orElse(null);
|
||||||
|
if (itemId.equals(id)) total += stack.getCount();
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,29 +4,33 @@ import net.neoforged.bus.api.IEventBus;
|
|||||||
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
|
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
|
||||||
import net.neoforged.neoforge.network.registration.PayloadRegistrar;
|
import net.neoforged.neoforge.network.registration.PayloadRegistrar;
|
||||||
import uk.sijbers.bnstoolkit.BnsToolkit;
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
import uk.sijbers.bnstoolkit.network.c2s.RequestTierStatePacket;
|
import uk.sijbers.bnstoolkit.network.c2s.RequestEconomySnapshotPacket;
|
||||||
|
import uk.sijbers.bnstoolkit.network.c2s.RunBnsCommandPacket;
|
||||||
|
import uk.sijbers.bnstoolkit.network.s2c.BountySnapshotPacket;
|
||||||
|
import uk.sijbers.bnstoolkit.network.s2c.PlotSnapshotPacket;
|
||||||
|
import uk.sijbers.bnstoolkit.network.s2c.SellSnapshotPacket;
|
||||||
import uk.sijbers.bnstoolkit.network.s2c.TierStateUpdatePacket;
|
import uk.sijbers.bnstoolkit.network.s2c.TierStateUpdatePacket;
|
||||||
|
import uk.sijbers.bnstoolkit.network.s2c.ToastPacket;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Central registration for the toolkit's S2C and C2S packets.
|
* Central registration for the toolkit's S2C and C2S packets.
|
||||||
*
|
*
|
||||||
* Spec ref: docs/bnstoolkit-architecture.md §5 (network contract).
|
* Two C2S packets cover the entire client-server contract:
|
||||||
|
* - {@link RequestEconomySnapshotPacket} — "send me my <kind> snapshot"
|
||||||
|
* - {@link RunBnsCommandPacket} — "run this whitelisted /bns verb"
|
||||||
*
|
*
|
||||||
* M0 ships one round-trip pair as a smoke-test of the wiring:
|
* Four S2C snapshots feed the screens + HUD:
|
||||||
* - C2S {@link RequestTierStatePacket} ("hey server, what's my tier+balance?")
|
* - {@link TierStateUpdatePacket} — tier + balance + fee
|
||||||
* - S2C {@link TierStateUpdatePacket} (answer + future push channel)
|
* - {@link SellSnapshotPacket} — lifetime-sold per item
|
||||||
|
* - {@link BountySnapshotPacket} — daily pool + active + completed + cap
|
||||||
|
* - {@link PlotSnapshotPacket} — owners + cap + my owned ids
|
||||||
*
|
*
|
||||||
* The rest of the contract (bounty / plot / shop / quest log) will land in
|
* Plus a fire-and-forget {@link ToastPacket} for command result UX.
|
||||||
* M1..M4 — each milestone adds its own packet pair under the matching
|
|
||||||
* sub-package.
|
|
||||||
*
|
|
||||||
* Versioning: bumped via the version() call below whenever the wire format
|
|
||||||
* changes incompatibly. Same protocol pattern FTB uses.
|
|
||||||
*/
|
*/
|
||||||
public final class BnsNetwork {
|
public final class BnsNetwork {
|
||||||
private BnsNetwork() {}
|
private BnsNetwork() {}
|
||||||
|
|
||||||
public static final String VERSION = "0.1";
|
public static final String VERSION = "0.2";
|
||||||
|
|
||||||
public static void register(IEventBus modBus) {
|
public static void register(IEventBus modBus) {
|
||||||
modBus.addListener(BnsNetwork::onRegisterPayloads);
|
modBus.addListener(BnsNetwork::onRegisterPayloads);
|
||||||
@@ -35,18 +39,35 @@ public final class BnsNetwork {
|
|||||||
private static void onRegisterPayloads(RegisterPayloadHandlersEvent event) {
|
private static void onRegisterPayloads(RegisterPayloadHandlersEvent event) {
|
||||||
final PayloadRegistrar reg = event.registrar(BnsToolkit.MOD_ID).versioned(VERSION);
|
final PayloadRegistrar reg = event.registrar(BnsToolkit.MOD_ID).versioned(VERSION);
|
||||||
|
|
||||||
// C2S: request my current tier + balance.
|
|
||||||
reg.playToServer(
|
reg.playToServer(
|
||||||
RequestTierStatePacket.TYPE,
|
RequestEconomySnapshotPacket.TYPE,
|
||||||
RequestTierStatePacket.STREAM_CODEC,
|
RequestEconomySnapshotPacket.STREAM_CODEC,
|
||||||
RequestTierStatePacket::handle);
|
RequestEconomySnapshotPacket::handle);
|
||||||
|
reg.playToServer(
|
||||||
|
RunBnsCommandPacket.TYPE,
|
||||||
|
RunBnsCommandPacket.STREAM_CODEC,
|
||||||
|
RunBnsCommandPacket::handle);
|
||||||
|
|
||||||
// S2C: server pushes tier + balance (either in response to a request
|
|
||||||
// or unsolicited when something changes).
|
|
||||||
reg.playToClient(
|
reg.playToClient(
|
||||||
TierStateUpdatePacket.TYPE,
|
TierStateUpdatePacket.TYPE,
|
||||||
TierStateUpdatePacket.STREAM_CODEC,
|
TierStateUpdatePacket.STREAM_CODEC,
|
||||||
TierStateUpdatePacket::handle);
|
TierStateUpdatePacket::handle);
|
||||||
|
reg.playToClient(
|
||||||
|
SellSnapshotPacket.TYPE,
|
||||||
|
SellSnapshotPacket.STREAM_CODEC,
|
||||||
|
SellSnapshotPacket::handle);
|
||||||
|
reg.playToClient(
|
||||||
|
BountySnapshotPacket.TYPE,
|
||||||
|
BountySnapshotPacket.STREAM_CODEC,
|
||||||
|
BountySnapshotPacket::handle);
|
||||||
|
reg.playToClient(
|
||||||
|
PlotSnapshotPacket.TYPE,
|
||||||
|
PlotSnapshotPacket.STREAM_CODEC,
|
||||||
|
PlotSnapshotPacket::handle);
|
||||||
|
reg.playToClient(
|
||||||
|
ToastPacket.TYPE,
|
||||||
|
ToastPacket.STREAM_CODEC,
|
||||||
|
ToastPacket::handle);
|
||||||
|
|
||||||
BnsToolkit.LOG.info("[bnstoolkit/net] registered payloads v{}", VERSION);
|
BnsToolkit.LOG.info("[bnstoolkit/net] registered payloads v{}", VERSION);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.network;
|
||||||
|
|
||||||
|
import net.neoforged.neoforge.network.PacketDistributor;
|
||||||
|
import uk.sijbers.bnstoolkit.network.c2s.RequestEconomySnapshotPacket;
|
||||||
|
import uk.sijbers.bnstoolkit.network.c2s.RunBnsCommandPacket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client-side senders for the two C2S packets. Centralised here so the
|
||||||
|
* screens don't all have to know the packet types.
|
||||||
|
*/
|
||||||
|
public final class NetSend {
|
||||||
|
private NetSend() {}
|
||||||
|
|
||||||
|
public static void requestSnapshot(String kind) {
|
||||||
|
PacketDistributor.sendToServer(new RequestEconomySnapshotPacket(kind));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void runBns(String args) {
|
||||||
|
PacketDistributor.sendToServer(new RunBnsCommandPacket(args));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.network;
|
||||||
|
|
||||||
|
import net.minecraft.commands.CommandSourceStack;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.nbt.ListTag;
|
||||||
|
import net.minecraft.nbt.Tag;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.neoforged.neoforge.network.PacketDistributor;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.data.Bounty;
|
||||||
|
import uk.sijbers.bnstoolkit.data.CivicTier;
|
||||||
|
import uk.sijbers.bnstoolkit.data.Plot;
|
||||||
|
import uk.sijbers.bnstoolkit.data.SpurBalance;
|
||||||
|
import uk.sijbers.bnstoolkit.network.s2c.BountySnapshotPacket;
|
||||||
|
import uk.sijbers.bnstoolkit.network.s2c.PlotSnapshotPacket;
|
||||||
|
import uk.sijbers.bnstoolkit.network.s2c.SellSnapshotPacket;
|
||||||
|
import uk.sijbers.bnstoolkit.network.s2c.TierStateUpdatePacket;
|
||||||
|
import uk.sijbers.bnstoolkit.network.s2c.ToastPacket;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-side bridge between the bnstoolkit network packets and the
|
||||||
|
* KubeJS-owned economy state.
|
||||||
|
*
|
||||||
|
* Reads: pull straight from player.persistentData (the NBT KubeJS writes).
|
||||||
|
* Writes: route through the existing /bns chat commands via the server's
|
||||||
|
* command stack — KubeJS is registered as a command-source handler,
|
||||||
|
* so this delegates business logic instead of re-implementing it.
|
||||||
|
*
|
||||||
|
* The verb whitelist below is what lets the screens trigger the SAME
|
||||||
|
* verbs the player can type — and nothing else.
|
||||||
|
*/
|
||||||
|
public final class ServerEconomyBridge {
|
||||||
|
private ServerEconomyBridge() {}
|
||||||
|
|
||||||
|
private static final Set<String> SAFE_VERBS = Set.of(
|
||||||
|
"tier upgrade",
|
||||||
|
"sell ", // followed by item id + count
|
||||||
|
"bounty accept ",
|
||||||
|
"bounty cancel ",
|
||||||
|
"bounty turnin ",
|
||||||
|
"plot buy ",
|
||||||
|
"plot release "
|
||||||
|
);
|
||||||
|
|
||||||
|
// ─── Routed commands ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static void runWhitelisted(Player player, String args) {
|
||||||
|
if (!(player instanceof ServerPlayer sp)) return;
|
||||||
|
if (args == null || args.isEmpty()) return;
|
||||||
|
|
||||||
|
boolean ok = false;
|
||||||
|
for (String prefix : SAFE_VERBS) {
|
||||||
|
if (args.equals(prefix.trim()) || args.startsWith(prefix)) { ok = true; break; }
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
BnsToolkit.LOG.warn("[bns/bridge] rejected non-whitelisted verb from {}: {}", sp.getName().getString(), args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandSourceStack src = sp.createCommandSourceStack();
|
||||||
|
String full = "bns " + args;
|
||||||
|
BnsToolkit.LOG.info("[bns/bridge] {} runs '{}'", sp.getName().getString(), full);
|
||||||
|
sp.server.getCommands().performPrefixedCommand(src, full);
|
||||||
|
|
||||||
|
// After any state-mutating verb, push a fresh snapshot relevant to it.
|
||||||
|
if (args.startsWith("tier")) {
|
||||||
|
pushTierUpdate(sp);
|
||||||
|
} else if (args.startsWith("sell")) {
|
||||||
|
pushTierUpdate(sp);
|
||||||
|
pushSellSnapshot(sp);
|
||||||
|
} else if (args.startsWith("bounty")) {
|
||||||
|
pushTierUpdate(sp);
|
||||||
|
pushBountySnapshot(sp);
|
||||||
|
} else if (args.startsWith("plot")) {
|
||||||
|
pushTierUpdate(sp);
|
||||||
|
pushPlotSnapshot(sp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Snapshot pushes ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
public static void pushSnapshot(Player player, String kind) {
|
||||||
|
if (!(player instanceof ServerPlayer sp)) return;
|
||||||
|
switch (kind) {
|
||||||
|
case "TIER" -> pushTierUpdate(sp);
|
||||||
|
case "SELL" -> { pushTierUpdate(sp); pushSellSnapshot(sp); }
|
||||||
|
case "BOUNTY" -> { pushTierUpdate(sp); pushBountySnapshot(sp); }
|
||||||
|
case "PLOT" -> { pushTierUpdate(sp); pushPlotSnapshot(sp); }
|
||||||
|
default -> { pushTierUpdate(sp); pushSellSnapshot(sp); pushBountySnapshot(sp); pushPlotSnapshot(sp); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void pushTierUpdate(ServerPlayer sp) {
|
||||||
|
CompoundTag data = sp.getPersistentData();
|
||||||
|
int tier = data.contains("bnsTier") ? data.getInt("bnsTier") : 1;
|
||||||
|
if (tier < 1 || tier > CivicTier.MAX) tier = 1;
|
||||||
|
CivicTier cfg = CivicTier.byId(tier);
|
||||||
|
long spurs = SpurBalance.totalSpurs(sp);
|
||||||
|
int fee = cfg.effectiveShopFeePct();
|
||||||
|
PacketDistributor.sendToPlayer(sp, new TierStateUpdatePacket(tier, spurs, fee));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void pushSellSnapshot(ServerPlayer sp) {
|
||||||
|
CompoundTag data = sp.getPersistentData();
|
||||||
|
Map<String, Long> sold = new HashMap<>();
|
||||||
|
if (data.contains("bnsSales")) {
|
||||||
|
CompoundTag map = data.getCompound("bnsSales");
|
||||||
|
for (String key : map.getAllKeys()) {
|
||||||
|
sold.put(key, (long) map.getInt(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PacketDistributor.sendToPlayer(sp, new SellSnapshotPacket(sold));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void pushBountySnapshot(ServerPlayer sp) {
|
||||||
|
CompoundTag data = sp.getPersistentData();
|
||||||
|
List<String> poolIds = new ArrayList<>();
|
||||||
|
Map<String, BountySnapshotPacket.ActiveEntry> active = new HashMap<>();
|
||||||
|
List<String> completed = new ArrayList<>();
|
||||||
|
|
||||||
|
if (data.contains("bnsBounties")) {
|
||||||
|
CompoundTag bnt = data.getCompound("bnsBounties");
|
||||||
|
if (bnt.contains("pool")) {
|
||||||
|
ListTag pool = bnt.getList("pool", Tag.TAG_STRING);
|
||||||
|
for (int i = 0; i < pool.size(); i++) poolIds.add(pool.getString(i));
|
||||||
|
}
|
||||||
|
if (bnt.contains("active")) {
|
||||||
|
CompoundTag act = bnt.getCompound("active");
|
||||||
|
for (String id : act.getAllKeys()) {
|
||||||
|
CompoundTag e = act.getCompound(id);
|
||||||
|
int progress = e.getInt("progress");
|
||||||
|
int target = e.getInt("target");
|
||||||
|
long reward = e.getInt("reward");
|
||||||
|
active.put(id, new BountySnapshotPacket.ActiveEntry(progress, target, reward));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bnt.contains("completed")) {
|
||||||
|
CompoundTag cmp = bnt.getCompound("completed");
|
||||||
|
for (String id : cmp.getAllKeys()) completed.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slot cap from tier
|
||||||
|
int tier = data.contains("bnsTier") ? data.getInt("bnsTier") : 1;
|
||||||
|
if (tier < 1 || tier > CivicTier.MAX) tier = 1;
|
||||||
|
int slotCap = CivicTier.byId(tier).dailyBountySlots();
|
||||||
|
|
||||||
|
PacketDistributor.sendToPlayer(sp, new BountySnapshotPacket(poolIds, active, completed, slotCap));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void pushPlotSnapshot(ServerPlayer sp) {
|
||||||
|
// Plot ownership is tracked in level.persistentData (world scope), not per-player.
|
||||||
|
// Read it via the world's saved data CompoundTag the KubeJS code writes to.
|
||||||
|
Map<String, String> owners = new HashMap<>();
|
||||||
|
for (Plot p : Plot.ALL) owners.put(p.id(), "");
|
||||||
|
|
||||||
|
net.minecraft.server.level.ServerLevel level = sp.serverLevel();
|
||||||
|
// KubeJS Persistent Data (world scope) lives at level.getDataStorage with key 'kubejs'
|
||||||
|
// but to avoid coupling, read from the per-player bnsOwnedPlots NBT array we set
|
||||||
|
// alongside ownership in plots.js. Fall back to empty.
|
||||||
|
List<String> ownedIds = new ArrayList<>();
|
||||||
|
CompoundTag pdata = sp.getPersistentData();
|
||||||
|
if (pdata.contains("bnsOwnedPlots")) {
|
||||||
|
ListTag list = pdata.getList("bnsOwnedPlots", Tag.TAG_STRING);
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
String id = list.getString(i);
|
||||||
|
ownedIds.add(id);
|
||||||
|
owners.put(id, sp.getName().getString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Mark plots owned by other players as taken — read everybody's
|
||||||
|
// persistentData via the level's player list.
|
||||||
|
for (ServerPlayer other : sp.server.getPlayerList().getPlayers()) {
|
||||||
|
if (other == sp) continue;
|
||||||
|
CompoundTag od = other.getPersistentData();
|
||||||
|
if (!od.contains("bnsOwnedPlots")) continue;
|
||||||
|
ListTag list = od.getList("bnsOwnedPlots", Tag.TAG_STRING);
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
String id = list.getString(i);
|
||||||
|
if (!ownedIds.contains(id)) owners.put(id, other.getName().getString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int tier = pdata.contains("bnsTier") ? pdata.getInt("bnsTier") : 1;
|
||||||
|
if (tier < 1 || tier > CivicTier.MAX) tier = 1;
|
||||||
|
int slotCap = CivicTier.byId(tier).plotSlots();
|
||||||
|
|
||||||
|
PacketDistributor.sendToPlayer(sp, new PlotSnapshotPacket(owners, slotCap, ownedIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void toast(ServerPlayer sp, int status, String title, String body) {
|
||||||
|
PacketDistributor.sendToPlayer(sp, new ToastPacket(status, title, body));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defensive: bounties pool format uses the same shape we know about
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private static Bounty resolveBounty(String id) { return Bounty.byId(id); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.network.c2s;
|
||||||
|
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.network.codec.ByteBufCodecs;
|
||||||
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.network.ServerEconomyBridge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client -> server: "Open my <kind> screen, send me the current snapshot."
|
||||||
|
*
|
||||||
|
* Kinds:
|
||||||
|
* TIER — push TierStateUpdatePacket
|
||||||
|
* SELL — push SellSnapshotPacket
|
||||||
|
* BOUNTY — push BountySnapshotPacket
|
||||||
|
* PLOT — push PlotSnapshotPacket
|
||||||
|
* ALL — push all four (e.g. on first login)
|
||||||
|
*
|
||||||
|
* Sending an enum int is more compact than a string but the count is
|
||||||
|
* tiny so we just use a String for debuggability.
|
||||||
|
*/
|
||||||
|
public record RequestEconomySnapshotPacket(String kind) implements CustomPacketPayload {
|
||||||
|
public static final Type<RequestEconomySnapshotPacket> TYPE = new Type<>(
|
||||||
|
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "request_snapshot"));
|
||||||
|
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf, RequestEconomySnapshotPacket> STREAM_CODEC =
|
||||||
|
StreamCodec.composite(
|
||||||
|
ByteBufCodecs.STRING_UTF8, RequestEconomySnapshotPacket::kind,
|
||||||
|
RequestEconomySnapshotPacket::new);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type<? extends CustomPacketPayload> type() {
|
||||||
|
return TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handle(RequestEconomySnapshotPacket pkt, IPayloadContext ctx) {
|
||||||
|
ctx.enqueueWork(() -> ServerEconomyBridge.pushSnapshot(ctx.player(), pkt.kind));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package uk.sijbers.bnstoolkit.network.c2s;
|
|
||||||
|
|
||||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
|
||||||
import net.minecraft.network.codec.StreamCodec;
|
|
||||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
|
||||||
import net.minecraft.resources.ResourceLocation;
|
|
||||||
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
|
||||||
import uk.sijbers.bnstoolkit.BnsToolkit;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client -> server: "Send me my tier + balance."
|
|
||||||
*
|
|
||||||
* Empty payload (no fields). The server identifies the player from
|
|
||||||
* {@link IPayloadContext#player()}. The response is a S2C
|
|
||||||
* {@code TierStateUpdatePacket}.
|
|
||||||
*
|
|
||||||
* TODO(matt M1): once the KubeJS bridge ships, the handler will:
|
|
||||||
* 1. Read player.persistentData.bnsTier (the KubeJS-owned NBT)
|
|
||||||
* 2. Sum Numismatics coin slots into a spurs total
|
|
||||||
* 3. Send a TierStateUpdatePacket back to the same player
|
|
||||||
*
|
|
||||||
* For now the handler just logs the request — proves the wire works.
|
|
||||||
*/
|
|
||||||
public record RequestTierStatePacket() implements CustomPacketPayload {
|
|
||||||
public static final Type<RequestTierStatePacket> TYPE = new Type<>(
|
|
||||||
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "request_tier_state"));
|
|
||||||
|
|
||||||
public static final StreamCodec<RegistryFriendlyByteBuf, RequestTierStatePacket> STREAM_CODEC =
|
|
||||||
StreamCodec.unit(new RequestTierStatePacket());
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Type<? extends CustomPacketPayload> type() {
|
|
||||||
return TYPE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void handle(RequestTierStatePacket pkt, IPayloadContext ctx) {
|
|
||||||
ctx.enqueueWork(() -> {
|
|
||||||
// TODO(matt M1): read NBT + Numismatics balance, reply with TierStateUpdatePacket
|
|
||||||
BnsToolkit.LOG.info("[bnstoolkit/net] RequestTierState from {}", ctx.player().getName().getString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.network.c2s;
|
||||||
|
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.network.codec.ByteBufCodecs;
|
||||||
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.network.ServerEconomyBridge;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client -> server: invoke one of the existing KubeJS-owned /bns
|
||||||
|
* subcommands on behalf of the player. Handler validates the verb against
|
||||||
|
* a whitelist, then runs it via the dedicated server's command stack so
|
||||||
|
* KubeJS picks it up exactly as if the player typed it.
|
||||||
|
*
|
||||||
|
* Examples sent by the screens:
|
||||||
|
* "tier upgrade"
|
||||||
|
* "sell minecraft:diamond 8"
|
||||||
|
* "bounty accept b_zombie_10"
|
||||||
|
* "bounty cancel b_zombie_10"
|
||||||
|
* "bounty turnin b_zombie_10"
|
||||||
|
* "plot buy p001"
|
||||||
|
* "plot release p001"
|
||||||
|
*
|
||||||
|
* The verb whitelist is enforced server-side in
|
||||||
|
* {@link ServerEconomyBridge#runWhitelisted}.
|
||||||
|
*/
|
||||||
|
public record RunBnsCommandPacket(String args) implements CustomPacketPayload {
|
||||||
|
public static final Type<RunBnsCommandPacket> TYPE = new Type<>(
|
||||||
|
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "run_bns"));
|
||||||
|
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf, RunBnsCommandPacket> STREAM_CODEC =
|
||||||
|
StreamCodec.composite(
|
||||||
|
ByteBufCodecs.STRING_UTF8, RunBnsCommandPacket::args,
|
||||||
|
RunBnsCommandPacket::new);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type<? extends CustomPacketPayload> type() {
|
||||||
|
return TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handle(RunBnsCommandPacket pkt, IPayloadContext ctx) {
|
||||||
|
ctx.enqueueWork(() -> ServerEconomyBridge.runWhitelisted(ctx.player(), pkt.args));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.network.s2c;
|
||||||
|
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.network.codec.ByteBufCodecs;
|
||||||
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.client.ClientBnsState;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S2C: today's bounty pool selection (5 ids) + the player's active set
|
||||||
|
* with kill progress + completed (cooldown) ids + the tier-derived slot cap.
|
||||||
|
*/
|
||||||
|
public record BountySnapshotPacket(
|
||||||
|
List<String> poolIds,
|
||||||
|
Map<String, ActiveEntry> active,
|
||||||
|
List<String> completedIds,
|
||||||
|
int slotCap) implements CustomPacketPayload {
|
||||||
|
|
||||||
|
public record ActiveEntry(int progress, int target, long reward) {}
|
||||||
|
|
||||||
|
public static final Type<BountySnapshotPacket> TYPE = new Type<>(
|
||||||
|
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "bounty_snapshot"));
|
||||||
|
|
||||||
|
private static final StreamCodec<RegistryFriendlyByteBuf, ActiveEntry> ENTRY_CODEC =
|
||||||
|
StreamCodec.composite(
|
||||||
|
ByteBufCodecs.VAR_INT, ActiveEntry::progress,
|
||||||
|
ByteBufCodecs.VAR_INT, ActiveEntry::target,
|
||||||
|
ByteBufCodecs.VAR_LONG, ActiveEntry::reward,
|
||||||
|
ActiveEntry::new);
|
||||||
|
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf, BountySnapshotPacket> STREAM_CODEC =
|
||||||
|
StreamCodec.composite(
|
||||||
|
ByteBufCodecs.STRING_UTF8.apply(ByteBufCodecs.list()), BountySnapshotPacket::poolIds,
|
||||||
|
ByteBufCodecs.map(HashMap::new, ByteBufCodecs.STRING_UTF8, ENTRY_CODEC), BountySnapshotPacket::active,
|
||||||
|
ByteBufCodecs.STRING_UTF8.apply(ByteBufCodecs.list()), BountySnapshotPacket::completedIds,
|
||||||
|
ByteBufCodecs.VAR_INT, BountySnapshotPacket::slotCap,
|
||||||
|
BountySnapshotPacket::new);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type<? extends CustomPacketPayload> type() {
|
||||||
|
return TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handle(BountySnapshotPacket pkt, IPayloadContext ctx) {
|
||||||
|
ctx.enqueueWork(() -> {
|
||||||
|
ClientBnsState.dailyPoolIds.clear();
|
||||||
|
ClientBnsState.dailyPoolIds.addAll(pkt.poolIds);
|
||||||
|
|
||||||
|
ClientBnsState.activeBounties.clear();
|
||||||
|
for (Map.Entry<String, ActiveEntry> e : pkt.active.entrySet()) {
|
||||||
|
ActiveEntry v = e.getValue();
|
||||||
|
ClientBnsState.activeBounties.put(e.getKey(),
|
||||||
|
new ClientBnsState.ActiveBounty(v.progress, v.target, v.reward));
|
||||||
|
}
|
||||||
|
ClientBnsState.completedIds.clear();
|
||||||
|
ClientBnsState.completedIds.addAll(pkt.completedIds);
|
||||||
|
ClientBnsState.bountySlotCap = pkt.slotCap;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.network.s2c;
|
||||||
|
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.network.codec.ByteBufCodecs;
|
||||||
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.client.ClientBnsState;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S2C: ownership state for every plot id, the player's slot cap (from tier),
|
||||||
|
* and the list of plot ids the player currently owns.
|
||||||
|
*
|
||||||
|
* `owners` value of "" means "available". Non-empty = current owner's name.
|
||||||
|
*/
|
||||||
|
public record PlotSnapshotPacket(
|
||||||
|
Map<String, String> owners,
|
||||||
|
int slotCap,
|
||||||
|
List<String> ownedIds) implements CustomPacketPayload {
|
||||||
|
|
||||||
|
public static final Type<PlotSnapshotPacket> TYPE = new Type<>(
|
||||||
|
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "plot_snapshot"));
|
||||||
|
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf, PlotSnapshotPacket> STREAM_CODEC =
|
||||||
|
StreamCodec.composite(
|
||||||
|
ByteBufCodecs.map(HashMap::new, ByteBufCodecs.STRING_UTF8, ByteBufCodecs.STRING_UTF8),
|
||||||
|
PlotSnapshotPacket::owners,
|
||||||
|
ByteBufCodecs.VAR_INT,
|
||||||
|
PlotSnapshotPacket::slotCap,
|
||||||
|
ByteBufCodecs.STRING_UTF8.apply(ByteBufCodecs.list()),
|
||||||
|
PlotSnapshotPacket::ownedIds,
|
||||||
|
PlotSnapshotPacket::new);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type<? extends CustomPacketPayload> type() {
|
||||||
|
return TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handle(PlotSnapshotPacket pkt, IPayloadContext ctx) {
|
||||||
|
ctx.enqueueWork(() -> {
|
||||||
|
ClientBnsState.plotOwners.clear();
|
||||||
|
ClientBnsState.plotOwners.putAll(pkt.owners);
|
||||||
|
ClientBnsState.plotSlotCap = pkt.slotCap;
|
||||||
|
ClientBnsState.ownedPlotIds.clear();
|
||||||
|
ClientBnsState.ownedPlotIds.addAll(pkt.ownedIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.network.s2c;
|
||||||
|
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.network.codec.ByteBufCodecs;
|
||||||
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.client.ClientBnsState;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S2C: lifetime-sold counts per item id, used by the shop screen to show
|
||||||
|
* accurate per-unit prices after diminishing returns.
|
||||||
|
*
|
||||||
|
* Wire format: map of {item id string -> lifetime sold count}.
|
||||||
|
*/
|
||||||
|
public record SellSnapshotPacket(Map<String, Long> sold) implements CustomPacketPayload {
|
||||||
|
public static final Type<SellSnapshotPacket> TYPE = new Type<>(
|
||||||
|
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "sell_snapshot"));
|
||||||
|
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf, SellSnapshotPacket> STREAM_CODEC =
|
||||||
|
StreamCodec.composite(
|
||||||
|
ByteBufCodecs.map(java.util.HashMap::new, ByteBufCodecs.STRING_UTF8, ByteBufCodecs.VAR_LONG),
|
||||||
|
SellSnapshotPacket::sold,
|
||||||
|
SellSnapshotPacket::new);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type<? extends CustomPacketPayload> type() {
|
||||||
|
return TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handle(SellSnapshotPacket pkt, IPayloadContext ctx) {
|
||||||
|
ctx.enqueueWork(() -> {
|
||||||
|
ClientBnsState.lifetimeSold.clear();
|
||||||
|
for (Map.Entry<String, Long> e : pkt.sold.entrySet()) {
|
||||||
|
try {
|
||||||
|
ClientBnsState.lifetimeSold.put(ResourceLocation.parse(e.getKey()), e.getValue());
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,20 +7,10 @@ import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
|||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
||||||
import uk.sijbers.bnstoolkit.BnsToolkit;
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
import uk.sijbers.bnstoolkit.client.ClientBnsState;
|
||||||
|
|
||||||
/**
|
/** Server -> client: civic tier (1..13), spurs balance, effective shop fee%. */
|
||||||
* Server -> client: pushes the player's current civic tier (1..13) and
|
public record TierStateUpdatePacket(int tier, long spurs, int effectiveFeePct) implements CustomPacketPayload {
|
||||||
* spurs balance.
|
|
||||||
*
|
|
||||||
* Sent in response to {@code RequestTierStatePacket} AND unsolicited
|
|
||||||
* whenever either value changes server-side (so the HUD updates in real
|
|
||||||
* time without polling).
|
|
||||||
*
|
|
||||||
* TODO(matt M1): client handler will cache the values in a static slot
|
|
||||||
* read by {@link uk.sijbers.bnstoolkit.client.hud.SpurHud} and any open
|
|
||||||
* screen.
|
|
||||||
*/
|
|
||||||
public record TierStateUpdatePacket(int tier, long spurs) implements CustomPacketPayload {
|
|
||||||
public static final Type<TierStateUpdatePacket> TYPE = new Type<>(
|
public static final Type<TierStateUpdatePacket> TYPE = new Type<>(
|
||||||
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "tier_state_update"));
|
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "tier_state_update"));
|
||||||
|
|
||||||
@@ -28,6 +18,7 @@ public record TierStateUpdatePacket(int tier, long spurs) implements CustomPacke
|
|||||||
StreamCodec.composite(
|
StreamCodec.composite(
|
||||||
ByteBufCodecs.VAR_INT, TierStateUpdatePacket::tier,
|
ByteBufCodecs.VAR_INT, TierStateUpdatePacket::tier,
|
||||||
ByteBufCodecs.VAR_LONG, TierStateUpdatePacket::spurs,
|
ByteBufCodecs.VAR_LONG, TierStateUpdatePacket::spurs,
|
||||||
|
ByteBufCodecs.VAR_INT, TierStateUpdatePacket::effectiveFeePct,
|
||||||
TierStateUpdatePacket::new);
|
TierStateUpdatePacket::new);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -37,8 +28,9 @@ public record TierStateUpdatePacket(int tier, long spurs) implements CustomPacke
|
|||||||
|
|
||||||
public static void handle(TierStateUpdatePacket pkt, IPayloadContext ctx) {
|
public static void handle(TierStateUpdatePacket pkt, IPayloadContext ctx) {
|
||||||
ctx.enqueueWork(() -> {
|
ctx.enqueueWork(() -> {
|
||||||
// TODO(matt M1): stash in a client-side state holder for HUD + screens.
|
ClientBnsState.tier = pkt.tier;
|
||||||
BnsToolkit.LOG.debug("[bnstoolkit/net] TierStateUpdate tier={} spurs={}", pkt.tier, pkt.spurs);
|
ClientBnsState.spurs = pkt.spurs;
|
||||||
|
ClientBnsState.effectiveFeePct = pkt.effectiveFeePct;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package uk.sijbers.bnstoolkit.network.s2c;
|
||||||
|
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.network.codec.ByteBufCodecs;
|
||||||
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.components.toasts.SystemToast;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import uk.sijbers.bnstoolkit.BnsToolkit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S2C: short toast notification to the player.
|
||||||
|
*
|
||||||
|
* Used by the server-side bridge to report results of routed /bns
|
||||||
|
* commands the player triggered from a screen (e.g. "Upgrade succeeded",
|
||||||
|
* "Insufficient funds", "Bounty turned in: +800 spurs").
|
||||||
|
*
|
||||||
|
* Status codes: 0=info, 1=success, 2=failure. The client maps these to
|
||||||
|
* SystemToast colour bands.
|
||||||
|
*/
|
||||||
|
public record ToastPacket(int status, String title, String body) implements CustomPacketPayload {
|
||||||
|
public static final Type<ToastPacket> TYPE = new Type<>(
|
||||||
|
ResourceLocation.fromNamespaceAndPath(BnsToolkit.MOD_ID, "toast"));
|
||||||
|
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf, ToastPacket> STREAM_CODEC =
|
||||||
|
StreamCodec.composite(
|
||||||
|
ByteBufCodecs.VAR_INT, ToastPacket::status,
|
||||||
|
ByteBufCodecs.STRING_UTF8, ToastPacket::title,
|
||||||
|
ByteBufCodecs.STRING_UTF8, ToastPacket::body,
|
||||||
|
ToastPacket::new);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type<? extends CustomPacketPayload> type() {
|
||||||
|
return TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void handle(ToastPacket pkt, IPayloadContext ctx) {
|
||||||
|
ctx.enqueueWork(() -> {
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
if (mc == null) return;
|
||||||
|
SystemToast.SystemToastId id = switch (pkt.status) {
|
||||||
|
case 1 -> SystemToast.SystemToastId.PERIODIC_NOTIFICATION;
|
||||||
|
case 2 -> SystemToast.SystemToastId.PACK_LOAD_FAILURE;
|
||||||
|
default -> SystemToast.SystemToastId.NARRATOR_TOGGLE;
|
||||||
|
};
|
||||||
|
mc.getToasts().addToast(SystemToast.multiline(mc, id,
|
||||||
|
Component.literal(pkt.title),
|
||||||
|
Component.literal(pkt.body)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,13 @@
|
|||||||
"key.category.bnstoolkit": "Brass and Sigil",
|
"key.category.bnstoolkit": "Brass and Sigil",
|
||||||
"key.bnstoolkit.tier": "Open Tier Ladder",
|
"key.bnstoolkit.tier": "Open Tier Ladder",
|
||||||
"key.bnstoolkit.bounties": "Open Bounty Board",
|
"key.bnstoolkit.bounties": "Open Bounty Board",
|
||||||
"key.bnstoolkit.plots": "Open Plot Map",
|
"key.bnstoolkit.plots": "Open Plot Office",
|
||||||
|
"key.bnstoolkit.shop": "Open Bazaar Shop",
|
||||||
|
"key.bnstoolkit.questlog": "Open Quest Log",
|
||||||
|
|
||||||
"screen.bnstoolkit.tier_upgrade": "Civic Tier",
|
"screen.bnstoolkit.tier_upgrade": "Civic Tier",
|
||||||
"screen.bnstoolkit.bounty_board": "Bounty Board",
|
"screen.bnstoolkit.bounty_board": "Bounty Board",
|
||||||
"screen.bnstoolkit.quest_log": "Quest Log",
|
"screen.bnstoolkit.quest_log": "Quest Log",
|
||||||
"screen.bnstoolkit.plot_purchase": "Plots",
|
"screen.bnstoolkit.plot_purchase": "Plot Office",
|
||||||
"screen.bnstoolkit.shop_browser": "Bazaar"
|
"screen.bnstoolkit.shop_browser": "Bazaar"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user