fix(contracts): wire home-page clear action and hide completed one-time contracts

This commit is contained in:
darwincereska
2026-05-09 11:44:39 -04:00
parent 024630d96c
commit 5f0085d2ce
8 changed files with 199 additions and 108 deletions
@@ -3,7 +3,6 @@ package com.g2806.soulsteal.contract;
import com.g2806.soulsteal.config.SoulStealConfig.ContractConfig;
import com.g2806.soulsteal.config.YamlConfigHelper;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -16,32 +15,20 @@ import java.util.Optional;
*/
public record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEnabled, String hudTitle, List<ContractDefinition> contracts) {
public static ContractCatalog fromMap(Map<String, Object> root, ContractConfig config) {
Map<String, Object> contractsSection = YamlConfigHelper.section(root, "contracts");
List<ContractDefinition> contracts = new ArrayList<>();
for (Map.Entry<String, Object> entry : contractsSection.entrySet()) {
if (!(entry.getValue() instanceof Map<?, ?> rawMap)) {
continue;
Object rawContracts = root.get("contracts");
if (rawContracts instanceof List<?> rawList) {
for (Object rawContract : rawList) {
addContract(contracts, rawContract);
}
Map<String, Object> map = toStringMap(rawMap);
String typeName = YamlConfigHelper.string(map, "type", "mining").trim().toUpperCase();
ContractType type;
try {
type = ContractType.valueOf(typeName);
} catch (IllegalArgumentException ignored) {
continue;
} else if (rawContracts instanceof Map<?, ?> rawSections) {
for (Map.Entry<?, ?> sectionEntry : rawSections.entrySet()) {
if (sectionEntry.getValue() instanceof List<?> rawList) {
for (Object rawContract : rawList) {
addContract(contracts, rawContract);
}
}
}
contracts.add(new ContractDefinition(
entry.getKey(),
YamlConfigHelper.string(map, "name", entry.getKey()),
YamlConfigHelper.string(map, "icon", type == ContractType.MINING ? "minecraft:iron_pickaxe" : "minecraft:zombie_head"),
type,
YamlConfigHelper.string(map, "target", ""),
YamlConfigHelper.string(map, "target_name", YamlConfigHelper.string(map, "target", entry.getKey())),
YamlConfigHelper.string(map, "description", ""),
Math.max(1L, YamlConfigHelper.longValue(map, "amount", 1L)),
Math.max(0L, YamlConfigHelper.longValue(map, "reward", 0L)),
YamlConfigHelper.bool(map, "repeatable", true)
));
}
return new ContractCatalog(config.enabled(), config.autoClaim(), config.hud().enabled(), config.hud().title(), contracts);
}
@@ -58,30 +45,32 @@ public record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEna
return """
contracts:
mining:
name: "Mining Contracts"
icon: "minecraft:iron_pickaxe"
type: "mining"
target: "minecraft:iron_ore"
target_name: "Iron Ore"
description: "Mine iron ore to earn souls."
amount: 64
reward: 250
repeatable: true
zombie_hunter:
name: "Zombie Hunter"
icon: "minecraft:zombie_head"
type: "hunting"
target: "minecraft:zombie"
target_name: "Zombie"
description: "Hunt zombies to earn souls."
amount: 20
reward: 200
repeatable: true
- id: "iron_miner"
name: "Mining Contracts"
icon: "minecraft:iron_pickaxe"
type: "mining"
target: "minecraft:iron_ore"
target_name: "Iron Ore"
description: "Mine iron ore to earn souls."
amount: 64
reward: 250
repeatable: true
hunting:
- id: "zombie_hunter"
name: "Zombie Hunter"
icon: "minecraft:zombie_head"
type: "hunting"
target: "minecraft:zombie"
target_name: "Zombie"
description: "Hunt zombies to earn souls."
amount: 20
reward: 200
repeatable: true
""";
}
private static Map<String, Object> toStringMap(Map<?, ?> rawMap) {
Map<String, Object> converted = new LinkedHashMap<>();
Map<String, Object> converted = new java.util.LinkedHashMap<>();
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
if (entry.getKey() != null) {
converted.put(String.valueOf(entry.getKey()), entry.getValue());
@@ -89,4 +78,36 @@ public record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEna
}
return converted;
}
private static void addContract(List<ContractDefinition> contracts, Object rawContract) {
if (!(rawContract instanceof Map<?, ?> rawMap)) {
return;
}
Map<String, Object> map = toStringMap(rawMap);
String id = YamlConfigHelper.string(map, "id", "").trim();
if (id.isBlank()) {
return;
}
String typeName = YamlConfigHelper.string(map, "type", "mining").trim().toUpperCase();
ContractType type;
try {
type = ContractType.valueOf(typeName);
} catch (IllegalArgumentException ignored) {
return;
}
contracts.add(new ContractDefinition(
id,
YamlConfigHelper.string(map, "name", id),
YamlConfigHelper.string(map, "icon", type == ContractType.MINING ? "minecraft:iron_pickaxe" : "minecraft:zombie_head"),
type,
YamlConfigHelper.string(map, "target", ""),
YamlConfigHelper.string(map, "target_name", YamlConfigHelper.string(map, "target", id)),
YamlConfigHelper.string(map, "description", ""),
Math.max(1L, YamlConfigHelper.longValue(map, "amount", 1L)),
Math.max(0L, YamlConfigHelper.longValue(map, "reward", 0L)),
YamlConfigHelper.bool(map, "repeatable", true)
));
}
}
@@ -25,7 +25,12 @@ import net.minecraft.util.Formatting;
*/
public final class ContractGuiService {
private static final int PAGE_ROWS = 6;
private static final int ITEM_SLOT_COUNT = 27;
private static final int ITEM_SLOT_COUNT = 45;
private static final int SLOT_HOME = 45;
private static final int SLOT_PREVIOUS = 46;
private static final int SLOT_INFO = 49;
private static final int SLOT_CLEAR = 52;
private static final int SLOT_NEXT = 53;
private final Supplier<ConfigBundle> bundleSupplier;
private final ContractService contractService;
private final RewardService rewardService;
@@ -94,41 +99,47 @@ public final class ContractGuiService {
private SimpleInventory createHomeInventory(ServerPlayerEntity player, HomeView view) {
SimpleInventory inventory = filledInventory(PAGE_ROWS);
List<ContractDefinition> mining = pagedContracts(bundleSupplier.get().contractCatalog().contractsOfType(ContractType.MINING), view.page());
List<ContractDefinition> hunting = pagedContracts(bundleSupplier.get().contractCatalog().contractsOfType(ContractType.HUNTING), view.page());
List<ContractDefinition> mining = bundleSupplier.get().contractCatalog().contractsOfType(ContractType.MINING);
List<ContractDefinition> hunting = bundleSupplier.get().contractCatalog().contractsOfType(ContractType.HUNTING);
inventory.setStack(11, createCategoryButton("Mining Contracts", "minecraft:iron_pickaxe", mining.size(), "Browse mining contracts"));
inventory.setStack(15, createCategoryButton("Hunting Contracts", "minecraft:zombie_head", hunting.size(), "Browse mob hunting contracts"));
inventory.setStack(4, createHomeInfoButton(player));
inventory.setStack(0, createCategoryButton("Mining Contracts", "minecraft:iron_pickaxe", mining.size(), "Browse mining contracts"));
inventory.setStack(1, createCategoryButton("Hunting Contracts", "minecraft:zombie_head", hunting.size(), "Browse mob hunting contracts"));
inventory.setStack(SLOT_INFO, createHomeInfoButton(player));
inventory.setStack(SLOT_CLEAR, createClearButton(player));
return inventory;
}
private SimpleInventory createCategoryInventory(ServerPlayerEntity player, CategoryView view) {
SimpleInventory inventory = filledInventory(PAGE_ROWS);
List<ContractDefinition> contracts = pagedContracts(contractsFor(view.categoryKey()), view.page());
List<ContractDefinition> contracts = pagedContracts(contractsFor(player, view.categoryKey()), view.page());
for (int index = 0; index < contracts.size() && index < ITEM_SLOT_COUNT; index++) {
inventory.setStack(index, createContractStack(player, contracts.get(index)));
}
inventory.setStack(ITEM_SLOT_COUNT, createBackButton());
inventory.setStack(ITEM_SLOT_COUNT + 1, createPageButton(view.page(), totalPages(view.categoryKey()), true));
inventory.setStack(ITEM_SLOT_COUNT + 4, createCategoryInfoButton(player, view.categoryKey()));
inventory.setStack(ITEM_SLOT_COUNT + 7, createPageButton(view.page(), totalPages(view.categoryKey()), false));
inventory.setStack(SLOT_HOME, createBackButton());
inventory.setStack(SLOT_PREVIOUS, createPageButton(view.page(), totalPages(player, view.categoryKey()), true));
inventory.setStack(SLOT_INFO, createCategoryInfoButton(player, view.categoryKey()));
inventory.setStack(SLOT_CLEAR, createClearButton(player));
inventory.setStack(SLOT_NEXT, createPageButton(view.page(), totalPages(player, view.categoryKey()), false));
return inventory;
}
private void handleHomeClick(ServerPlayerEntity player, HomeView view, int slotIndex) {
if (slotIndex == 11) {
if (slotIndex == 0) {
openCategory(player, "mining", 0);
} else if (slotIndex == 15) {
} else if (slotIndex == 1) {
openCategory(player, "hunting", 0);
} else if (slotIndex == SLOT_CLEAR) {
clearSelection(player);
player.sendMessage(net.minecraft.text.Text.literal("Cleared selected contract.").formatted(Formatting.GREEN), false);
openHome(player, view.page());
}
}
private void handleCategoryClick(ServerPlayerEntity player, CategoryView view, int slotIndex) {
if (slotIndex < ITEM_SLOT_COUNT) {
List<ContractDefinition> contracts = pagedContracts(contractsFor(view.categoryKey()), view.page());
List<ContractDefinition> contracts = pagedContracts(contractsFor(player, view.categoryKey()), view.page());
if (slotIndex >= contracts.size()) {
return;
}
@@ -140,12 +151,16 @@ public final class ContractGuiService {
return;
}
if (slotIndex == ITEM_SLOT_COUNT) {
if (slotIndex == SLOT_HOME) {
openContracts(player);
} else if (slotIndex == ITEM_SLOT_COUNT + 1) {
} else if (slotIndex == SLOT_PREVIOUS) {
openCategory(player, view.categoryKey(), Math.max(0, view.page() - 1));
} else if (slotIndex == ITEM_SLOT_COUNT + 7) {
openCategory(player, view.categoryKey(), Math.min(totalPages(view.categoryKey()) - 1, view.page() + 1));
} else if (slotIndex == SLOT_NEXT) {
openCategory(player, view.categoryKey(), Math.min(totalPages(player, view.categoryKey()) - 1, view.page() + 1));
} else if (slotIndex == SLOT_CLEAR) {
clearSelection(player);
player.sendMessage(net.minecraft.text.Text.literal("Cleared selected contract.").formatted(Formatting.GREEN), false);
openCategory(player, view.categoryKey(), view.page());
}
}
@@ -187,7 +202,7 @@ public final class ContractGuiService {
}
private ItemStack createCategoryInfoButton(ServerPlayerEntity player, String categoryKey) {
List<ContractDefinition> contracts = contractsFor(categoryKey);
List<ContractDefinition> contracts = contractsFor(player, categoryKey);
return createPreviewStack("minecraft:nether_star", categoryLabel(categoryKey), List.of(
Text.literal("Contracts: " + contracts.size()).formatted(Formatting.AQUA),
Text.literal("Selected: " + (selected(player) == null ? "None" : selected(player).name())).formatted(Formatting.GOLD)
@@ -198,6 +213,14 @@ public final class ContractGuiService {
return createPreviewStack("minecraft:barrier", "Back", List.of(Text.literal("Return to the contract browser.").formatted(Formatting.GRAY)));
}
private ItemStack createClearButton(ServerPlayerEntity player) {
ContractDefinition selected = selected(player);
return createPreviewStack("minecraft:redstone_torch", "Clear Selected", List.of(
Text.literal(selected == null ? "No contract selected." : "Clear: " + selected.name()).formatted(Formatting.GRAY),
Text.literal("Remove your active contract.").formatted(Formatting.DARK_GRAY)
));
}
private ItemStack createPageButton(int page, int totalPages, boolean previous) {
boolean available = previous ? page > 0 : page < totalPages - 1;
String label = previous ? "Previous Page" : "Next Page";
@@ -207,12 +230,15 @@ public final class ContractGuiService {
));
}
private List<ContractDefinition> contractsFor(String categoryKey) {
return switch (categoryKey) {
private List<ContractDefinition> contractsFor(ServerPlayerEntity player, String categoryKey) {
List<ContractDefinition> contracts = switch (categoryKey) {
case "mining" -> bundleSupplier.get().contractCatalog().contractsOfType(ContractType.MINING);
case "hunting" -> bundleSupplier.get().contractCatalog().contractsOfType(ContractType.HUNTING);
default -> List.of();
};
return contracts.stream()
.filter(contract -> contract.repeatable() || !contractService.hasCompletedContract(player.getUuid(), contract.id()))
.toList();
}
private List<ContractDefinition> pagedContracts(List<ContractDefinition> contracts, int page) {
@@ -224,8 +250,8 @@ public final class ContractGuiService {
return contracts.subList(from, to);
}
private int totalPages(String categoryKey) {
return Math.max(1, (int) Math.ceil(contractsFor(categoryKey).size() / (double) ITEM_SLOT_COUNT));
private int totalPages(ServerPlayerEntity player, String categoryKey) {
return Math.max(1, (int) Math.ceil(contractsFor(player, categoryKey).size() / (double) ITEM_SLOT_COUNT));
}
private String categoryLabel(String categoryKey) {
@@ -24,6 +24,7 @@ public final class SoulStealData {
private Map<String, Boolean> scoreboardVisibility = new HashMap<>();
private Map<String, String> selectedContracts = new HashMap<>();
private Map<String, Map<String, Long>> contractProgress = new HashMap<>();
private Map<String, Set<String>> completedContracts = new HashMap<>();
public SoulStealData normalize() {
if (souls == null) {
@@ -56,6 +57,9 @@ public final class SoulStealData {
if (contractProgress == null) {
contractProgress = new HashMap<>();
}
if (completedContracts == null) {
completedContracts = new HashMap<>();
}
return this;
}
@@ -138,4 +142,8 @@ public final class SoulStealData {
public Map<String, Map<String, Long>> contractProgress() {
return contractProgress;
}
public Map<String, Set<String>> completedContracts() {
return completedContracts;
}
}
@@ -36,6 +36,9 @@ public final class ContractService {
if (contract.isEmpty()) {
return false;
}
if (!contract.get().repeatable() && hasCompletedContract(player.getUuid(), contract.get().id())) {
return false;
}
dataStore.data().selectedContracts().put(key(player.getUuid()), contract.get().id());
dataStore.data().contractProgress().remove(key(player.getUuid()));
saveQuietly();
@@ -60,6 +63,12 @@ public final class ContractService {
.getOrDefault(contractId, 0L);
}
public boolean hasCompletedContract(UUID playerUuid, String contractId) {
return dataStore.data().completedContracts()
.getOrDefault(key(playerUuid), java.util.Set.of())
.contains(contractId);
}
public void recordMining(ServerPlayerEntity player, String blockId) {
record(player, ContractType.MINING, blockId);
}
@@ -86,6 +95,11 @@ public final class ContractService {
soulService.addSouls(player.getUuid(), contract.reward());
player.sendMessage(net.minecraft.text.Text.literal("Contract complete: " + contract.name() + " (+"
+ contract.reward() + " souls)").formatted(net.minecraft.util.Formatting.GREEN), false);
if (!contract.repeatable()) {
dataStore.data().completedContracts()
.computeIfAbsent(playerKey, ignored -> new java.util.HashSet<>())
.add(contract.id());
}
if (!contract.repeatable()) {
dataStore.data().selectedContracts().remove(playerKey);
}
@@ -25,13 +25,14 @@ import net.minecraft.network.packet.s2c.play.ScoreboardObjectiveUpdateS2CPacket;
import net.minecraft.network.packet.s2c.play.ScoreboardScoreResetS2CPacket;
import net.minecraft.network.packet.s2c.play.ScoreboardScoreUpdateS2CPacket;
import net.minecraft.scoreboard.Scoreboard;
import net.minecraft.scoreboard.ScoreboardCriterion;
import net.minecraft.scoreboard.ScoreboardDisplaySlot;
import net.minecraft.scoreboard.ScoreboardObjective;
import net.minecraft.scoreboard.number.BlankNumberFormat;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.scoreboard.ScoreboardCriterion;
import net.minecraft.scoreboard.ScoreboardDisplaySlot;
import net.minecraft.scoreboard.ScoreboardObjective;
import net.minecraft.scoreboard.number.BlankNumberFormat;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
/** Owns toggleable HUD state, player names, leaderboard data, and wanted-player bossbars. */
public final class HudService {
@@ -239,27 +240,31 @@ public final class HudService {
}
}
private List<Text> buildSidebarLines(ServerPlayerEntity player, long nowEpochMillis) {
List<StoredBounty> activeBounties = bountyService.activeBountiesForTarget(player.getUuid());
long totalValue = activeBounties.stream().mapToLong(StoredBounty::soulValue).sum();
long remainingSeconds = activeBounties.stream()
.mapToLong(StoredBounty::expiresAtEpochMillis)
.max()
.orElse(nowEpochMillis);
remainingSeconds = Math.max(0L, (remainingSeconds - nowEpochMillis + 999L) / 1000L);
return List.of(
Text.literal("Souls: " + soulService.balanceOf(player.getUuid())),
Text.literal(contractService.selectedContract(player.getUuid())
.map(contract -> "Contract: " + contract.name())
.orElse("Contract: None")),
Text.literal(contractService.selectedContract(player.getUuid())
.map(contract -> "Progress: " + contractService.progress(player.getUuid(), contract.id()) + "/" + contract.amountRequired())
.orElse("Progress: -")),
Text.literal("Bounties: " + activeBounties.size()),
Text.literal("Wanted Value: " + totalValue),
Text.literal("Wanted Time: " + (remainingSeconds > 0L ? DurationFormatter.formatSeconds(remainingSeconds) : "None"))
);
private List<Text> buildSidebarLines(ServerPlayerEntity player, long nowEpochMillis) {
List<StoredBounty> activeBounties = bountyService.activeBountiesForTarget(player.getUuid());
List<Text> lines = new ArrayList<>();
lines.add(Text.literal("Souls: " + soulService.balanceOf(player.getUuid())).formatted(Formatting.GOLD));
contractService.selectedContract(player.getUuid()).ifPresent(contract -> {
long progress = contractService.progress(player.getUuid(), contract.id());
lines.add(Text.literal("Contract: " + contract.name()).formatted(Formatting.AQUA));
lines.add(Text.literal("Progress: " + progress + "/" + contract.amountRequired()).formatted(Formatting.GRAY));
});
if (!activeBounties.isEmpty()) {
long totalValue = activeBounties.stream().mapToLong(StoredBounty::soulValue).sum();
long remainingSeconds = activeBounties.stream()
.mapToLong(StoredBounty::expiresAtEpochMillis)
.max()
.orElse(nowEpochMillis);
remainingSeconds = Math.max(0L, (remainingSeconds - nowEpochMillis + 999L) / 1000L);
lines.add(Text.literal("Bounties: " + activeBounties.size()).formatted(Formatting.RED));
lines.add(Text.literal("Wanted Value: " + totalValue).formatted(Formatting.GOLD));
lines.add(Text.literal("Wanted Time: " + DurationFormatter.formatSeconds(remainingSeconds)).formatted(Formatting.DARK_RED));
}
return lines;
}
private void clearSidebar(ServerPlayerEntity player) {
@@ -286,14 +291,14 @@ public final class HudService {
private ScoreboardObjective createObjective(String objectiveName) {
Scoreboard scoreboard = new Scoreboard();
return scoreboard.addObjective(
objectiveName,
ScoreboardCriterion.DUMMY,
Text.literal(configSupplier.get().hud().scoreboard().title()),
ScoreboardCriterion.RenderType.INTEGER,
false,
BlankNumberFormat.INSTANCE
);
return scoreboard.addObjective(
objectiveName,
ScoreboardCriterion.DUMMY,
Text.literal(configSupplier.get().hud().scoreboard().title()).formatted(Formatting.DARK_AQUA),
ScoreboardCriterion.RenderType.INTEGER,
false,
BlankNumberFormat.INSTANCE
);
}
private void rememberPlayer(ServerPlayerEntity player) {