fix(contracts): wire home-page clear action and hide completed one-time contracts
This commit is contained in:
@@ -15,7 +15,7 @@ Full project documentation is available in [docs/PROJECT.md](docs/PROJECT.md).
|
||||
|
||||
## How It Works
|
||||
|
||||
Players gain souls for killing other players and lose souls whenever they die, with all values driven by `config.yml`. The bounty system lets players spend souls to place timed bounties that pay killers on claim or reward survivors on expiry, while wanted players can see a bounty timer bossbar. The optional HUD sidebar can be toggled per player, and `/souls top` shows the configured leaderboard. The shop is a server-side chest GUI with a category home page, arrow pagination, optional reward display names, and item listings that can open a quantity selector.
|
||||
Players gain souls for killing other players and lose souls whenever they die, with all values driven by `config.yml`. The bounty system lets players spend souls to place timed bounties that pay killers on claim or reward survivors on expiry, while wanted players can see a bounty timer bossbar. The optional HUD sidebar can be toggled per player, uses the same dark aqua title styling as the mod prefix, color-codes the visible lines, and only shows contract or bounty rows when they are actually active. `/souls top` shows the configured leaderboard. The shop is a server-side chest GUI with a category home page, arrow pagination, optional reward display names, and item listings that can open a quantity selector.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -42,5 +42,5 @@ Players gain souls for killing other players and lose souls whenever they die, w
|
||||
| --- | --- |
|
||||
| `config/soulsteal/config.yml` | Economy values, death penalties, bounty limits, HUD toggles, leaderboard size, bossbar text, and command permission nodes. |
|
||||
| `config/soulsteal/shop.yml` | Shop categories, GUI entries, prices, cooldowns, reward display names, and optional custom-amount settings for item listings. |
|
||||
| `config/soulsteal/catalog.yml` | Mining and hunting contract entries, internal ids, player-facing names, icons, targets, progress amounts, and rewards. |
|
||||
| `config/soulsteal/catalog.yml` | Grouped mining and hunting contract sections, internal ids, player-facing names, icons, targets, progress amounts, rewards, and repeatable/one-time behavior. |
|
||||
| `config/soulsteal/soulsteal-data.json` | Persistent balances, active bounties, cooldowns, unlocks, internal permission fallback storage, saved player names, and scoreboard preferences. |
|
||||
|
||||
+19
-2
@@ -86,7 +86,11 @@ Behavior:
|
||||
- Hunting contracts track kills against a configured entity id.
|
||||
- The selected contract and its progress appear in the HUD sidebar.
|
||||
- Completing a contract pays souls automatically.
|
||||
- One-time contracts disappear from the browser after completion and cannot be selected again.
|
||||
- The contract browser uses `catalog.yml`, where the YAML key is the internal contract id and `name` is the player-facing label.
|
||||
- Contract entries are organized under `contracts: mining:` and `contracts: hunting:` sections for readability, while still using per-entry ids.
|
||||
- The contract browser uses a bottom control row like the shop, including page navigation anchored to the bottom of the inventory.
|
||||
- The contract browser includes a clear-selection action on the home page and inside contract categories so players can remove their active contract from the GUI.
|
||||
|
||||
Implemented by:
|
||||
|
||||
@@ -122,6 +126,9 @@ The HUD layer is optional and configurable per player.
|
||||
Behavior:
|
||||
|
||||
- Scoreboard sidebar can be enabled globally and toggled per player.
|
||||
- The sidebar title uses the same dark aqua styling as the mod's chat prefix.
|
||||
- Contract and bounty rows only appear while the player has an active selected contract or active bounties.
|
||||
- Balance, contract, and bounty rows use color to make the sidebar easier to scan.
|
||||
- Leaderboard pages are built from stored player names and balance values.
|
||||
- Wanted-player bossbars show bounty value and remaining time.
|
||||
|
||||
@@ -231,8 +238,8 @@ The catalog is loaded through [`ConfigBundle`](../src/main/java/com/g2806/soulst
|
||||
|
||||
Contract catalog definition. It controls:
|
||||
|
||||
- Contract categories by type
|
||||
- Internal contract ids from the YAML keys
|
||||
- Grouped contract lists under `mining` and `hunting`
|
||||
- Internal contract ids from each entry `id`
|
||||
- Player-facing contract names
|
||||
- Icon item ids
|
||||
- Target block or mob ids
|
||||
@@ -268,6 +275,16 @@ When a player joins:
|
||||
2. Their HUD state is refreshed.
|
||||
3. Any configured scoreboard or bossbar state is pushed to them.
|
||||
|
||||
### HUD Sidebar
|
||||
|
||||
When the sidebar is visible:
|
||||
|
||||
1. The title is rendered in dark aqua.
|
||||
2. The soul balance row is always shown.
|
||||
3. Contract rows are only shown if the player has an active selected contract.
|
||||
4. Bounty rows are only shown if the player currently has active bounties.
|
||||
5. The visible rows use color to distinguish balance, contract, and bounty information.
|
||||
|
||||
### Player Kill
|
||||
|
||||
When one player kills another player:
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ loom_version=1.16.1
|
||||
fabric_api_version=0.141.3+1.21.11
|
||||
|
||||
# Mod Properties
|
||||
mod_version=0.3.0
|
||||
mod_version=0.4.0
|
||||
maven_group=com.g2806.soulsteal
|
||||
archives_base_name=soul-steal
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ 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 {
|
||||
@@ -241,25 +242,29 @@ 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);
|
||||
List<Text> lines = new ArrayList<>();
|
||||
lines.add(Text.literal("Souls: " + soulService.balanceOf(player.getUuid())).formatted(Formatting.GOLD));
|
||||
|
||||
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"))
|
||||
);
|
||||
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) {
|
||||
@@ -289,7 +294,7 @@ public final class HudService {
|
||||
return scoreboard.addObjective(
|
||||
objectiveName,
|
||||
ScoreboardCriterion.DUMMY,
|
||||
Text.literal(configSupplier.get().hud().scoreboard().title()),
|
||||
Text.literal(configSupplier.get().hud().scoreboard().title()).formatted(Formatting.DARK_AQUA),
|
||||
ScoreboardCriterion.RenderType.INTEGER,
|
||||
false,
|
||||
BlankNumberFormat.INSTANCE
|
||||
|
||||
Reference in New Issue
Block a user