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
|
## 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
|
## 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/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/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. |
|
| `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.
|
- Hunting contracts track kills against a configured entity id.
|
||||||
- The selected contract and its progress appear in the HUD sidebar.
|
- The selected contract and its progress appear in the HUD sidebar.
|
||||||
- Completing a contract pays souls automatically.
|
- 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.
|
- 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:
|
Implemented by:
|
||||||
|
|
||||||
@@ -122,6 +126,9 @@ The HUD layer is optional and configurable per player.
|
|||||||
Behavior:
|
Behavior:
|
||||||
|
|
||||||
- Scoreboard sidebar can be enabled globally and toggled per player.
|
- 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.
|
- Leaderboard pages are built from stored player names and balance values.
|
||||||
- Wanted-player bossbars show bounty value and remaining time.
|
- 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 catalog definition. It controls:
|
||||||
|
|
||||||
- Contract categories by type
|
- Grouped contract lists under `mining` and `hunting`
|
||||||
- Internal contract ids from the YAML keys
|
- Internal contract ids from each entry `id`
|
||||||
- Player-facing contract names
|
- Player-facing contract names
|
||||||
- Icon item ids
|
- Icon item ids
|
||||||
- Target block or mob ids
|
- Target block or mob ids
|
||||||
@@ -268,6 +275,16 @@ When a player joins:
|
|||||||
2. Their HUD state is refreshed.
|
2. Their HUD state is refreshed.
|
||||||
3. Any configured scoreboard or bossbar state is pushed to them.
|
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
|
### Player Kill
|
||||||
|
|
||||||
When one player kills another player:
|
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
|
fabric_api_version=0.141.3+1.21.11
|
||||||
|
|
||||||
# Mod Properties
|
# Mod Properties
|
||||||
mod_version=0.3.0
|
mod_version=0.4.0
|
||||||
maven_group=com.g2806.soulsteal
|
maven_group=com.g2806.soulsteal
|
||||||
archives_base_name=soul-steal
|
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.SoulStealConfig.ContractConfig;
|
||||||
import com.g2806.soulsteal.config.YamlConfigHelper;
|
import com.g2806.soulsteal.config.YamlConfigHelper;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
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 record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEnabled, String hudTitle, List<ContractDefinition> contracts) {
|
||||||
public static ContractCatalog fromMap(Map<String, Object> root, ContractConfig config) {
|
public static ContractCatalog fromMap(Map<String, Object> root, ContractConfig config) {
|
||||||
Map<String, Object> contractsSection = YamlConfigHelper.section(root, "contracts");
|
|
||||||
List<ContractDefinition> contracts = new ArrayList<>();
|
List<ContractDefinition> contracts = new ArrayList<>();
|
||||||
for (Map.Entry<String, Object> entry : contractsSection.entrySet()) {
|
Object rawContracts = root.get("contracts");
|
||||||
if (!(entry.getValue() instanceof Map<?, ?> rawMap)) {
|
if (rawContracts instanceof List<?> rawList) {
|
||||||
continue;
|
for (Object rawContract : rawList) {
|
||||||
|
addContract(contracts, rawContract);
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
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);
|
return new ContractCatalog(config.enabled(), config.autoClaim(), config.hud().enabled(), config.hud().title(), contracts);
|
||||||
}
|
}
|
||||||
@@ -58,6 +45,7 @@ public record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEna
|
|||||||
return """
|
return """
|
||||||
contracts:
|
contracts:
|
||||||
mining:
|
mining:
|
||||||
|
- id: "iron_miner"
|
||||||
name: "Mining Contracts"
|
name: "Mining Contracts"
|
||||||
icon: "minecraft:iron_pickaxe"
|
icon: "minecraft:iron_pickaxe"
|
||||||
type: "mining"
|
type: "mining"
|
||||||
@@ -67,7 +55,8 @@ public record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEna
|
|||||||
amount: 64
|
amount: 64
|
||||||
reward: 250
|
reward: 250
|
||||||
repeatable: true
|
repeatable: true
|
||||||
zombie_hunter:
|
hunting:
|
||||||
|
- id: "zombie_hunter"
|
||||||
name: "Zombie Hunter"
|
name: "Zombie Hunter"
|
||||||
icon: "minecraft:zombie_head"
|
icon: "minecraft:zombie_head"
|
||||||
type: "hunting"
|
type: "hunting"
|
||||||
@@ -81,7 +70,7 @@ public record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEna
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Map<String, Object> toStringMap(Map<?, ?> rawMap) {
|
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()) {
|
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
|
||||||
if (entry.getKey() != null) {
|
if (entry.getKey() != null) {
|
||||||
converted.put(String.valueOf(entry.getKey()), entry.getValue());
|
converted.put(String.valueOf(entry.getKey()), entry.getValue());
|
||||||
@@ -89,4 +78,36 @@ public record ContractCatalog(boolean enabled, boolean autoClaim, boolean hudEna
|
|||||||
}
|
}
|
||||||
return converted;
|
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 {
|
public final class ContractGuiService {
|
||||||
private static final int PAGE_ROWS = 6;
|
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 Supplier<ConfigBundle> bundleSupplier;
|
||||||
private final ContractService contractService;
|
private final ContractService contractService;
|
||||||
private final RewardService rewardService;
|
private final RewardService rewardService;
|
||||||
@@ -94,41 +99,47 @@ public final class ContractGuiService {
|
|||||||
|
|
||||||
private SimpleInventory createHomeInventory(ServerPlayerEntity player, HomeView view) {
|
private SimpleInventory createHomeInventory(ServerPlayerEntity player, HomeView view) {
|
||||||
SimpleInventory inventory = filledInventory(PAGE_ROWS);
|
SimpleInventory inventory = filledInventory(PAGE_ROWS);
|
||||||
List<ContractDefinition> mining = pagedContracts(bundleSupplier.get().contractCatalog().contractsOfType(ContractType.MINING), view.page());
|
List<ContractDefinition> mining = bundleSupplier.get().contractCatalog().contractsOfType(ContractType.MINING);
|
||||||
List<ContractDefinition> hunting = pagedContracts(bundleSupplier.get().contractCatalog().contractsOfType(ContractType.HUNTING), view.page());
|
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(0, 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(1, createCategoryButton("Hunting Contracts", "minecraft:zombie_head", hunting.size(), "Browse mob hunting contracts"));
|
||||||
inventory.setStack(4, createHomeInfoButton(player));
|
inventory.setStack(SLOT_INFO, createHomeInfoButton(player));
|
||||||
|
inventory.setStack(SLOT_CLEAR, createClearButton(player));
|
||||||
return inventory;
|
return inventory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private SimpleInventory createCategoryInventory(ServerPlayerEntity player, CategoryView view) {
|
private SimpleInventory createCategoryInventory(ServerPlayerEntity player, CategoryView view) {
|
||||||
SimpleInventory inventory = filledInventory(PAGE_ROWS);
|
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++) {
|
for (int index = 0; index < contracts.size() && index < ITEM_SLOT_COUNT; index++) {
|
||||||
inventory.setStack(index, createContractStack(player, contracts.get(index)));
|
inventory.setStack(index, createContractStack(player, contracts.get(index)));
|
||||||
}
|
}
|
||||||
|
|
||||||
inventory.setStack(ITEM_SLOT_COUNT, createBackButton());
|
inventory.setStack(SLOT_HOME, createBackButton());
|
||||||
inventory.setStack(ITEM_SLOT_COUNT + 1, createPageButton(view.page(), totalPages(view.categoryKey()), true));
|
inventory.setStack(SLOT_PREVIOUS, createPageButton(view.page(), totalPages(player, view.categoryKey()), true));
|
||||||
inventory.setStack(ITEM_SLOT_COUNT + 4, createCategoryInfoButton(player, view.categoryKey()));
|
inventory.setStack(SLOT_INFO, createCategoryInfoButton(player, view.categoryKey()));
|
||||||
inventory.setStack(ITEM_SLOT_COUNT + 7, createPageButton(view.page(), totalPages(view.categoryKey()), false));
|
inventory.setStack(SLOT_CLEAR, createClearButton(player));
|
||||||
|
inventory.setStack(SLOT_NEXT, createPageButton(view.page(), totalPages(player, view.categoryKey()), false));
|
||||||
return inventory;
|
return inventory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleHomeClick(ServerPlayerEntity player, HomeView view, int slotIndex) {
|
private void handleHomeClick(ServerPlayerEntity player, HomeView view, int slotIndex) {
|
||||||
if (slotIndex == 11) {
|
if (slotIndex == 0) {
|
||||||
openCategory(player, "mining", 0);
|
openCategory(player, "mining", 0);
|
||||||
} else if (slotIndex == 15) {
|
} else if (slotIndex == 1) {
|
||||||
openCategory(player, "hunting", 0);
|
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) {
|
private void handleCategoryClick(ServerPlayerEntity player, CategoryView view, int slotIndex) {
|
||||||
if (slotIndex < ITEM_SLOT_COUNT) {
|
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()) {
|
if (slotIndex >= contracts.size()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -140,12 +151,16 @@ public final class ContractGuiService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (slotIndex == ITEM_SLOT_COUNT) {
|
if (slotIndex == SLOT_HOME) {
|
||||||
openContracts(player);
|
openContracts(player);
|
||||||
} else if (slotIndex == ITEM_SLOT_COUNT + 1) {
|
} else if (slotIndex == SLOT_PREVIOUS) {
|
||||||
openCategory(player, view.categoryKey(), Math.max(0, view.page() - 1));
|
openCategory(player, view.categoryKey(), Math.max(0, view.page() - 1));
|
||||||
} else if (slotIndex == ITEM_SLOT_COUNT + 7) {
|
} else if (slotIndex == SLOT_NEXT) {
|
||||||
openCategory(player, view.categoryKey(), Math.min(totalPages(view.categoryKey()) - 1, view.page() + 1));
|
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) {
|
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(
|
return createPreviewStack("minecraft:nether_star", categoryLabel(categoryKey), List.of(
|
||||||
Text.literal("Contracts: " + contracts.size()).formatted(Formatting.AQUA),
|
Text.literal("Contracts: " + contracts.size()).formatted(Formatting.AQUA),
|
||||||
Text.literal("Selected: " + (selected(player) == null ? "None" : selected(player).name())).formatted(Formatting.GOLD)
|
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)));
|
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) {
|
private ItemStack createPageButton(int page, int totalPages, boolean previous) {
|
||||||
boolean available = previous ? page > 0 : page < totalPages - 1;
|
boolean available = previous ? page > 0 : page < totalPages - 1;
|
||||||
String label = previous ? "Previous Page" : "Next Page";
|
String label = previous ? "Previous Page" : "Next Page";
|
||||||
@@ -207,12 +230,15 @@ public final class ContractGuiService {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ContractDefinition> contractsFor(String categoryKey) {
|
private List<ContractDefinition> contractsFor(ServerPlayerEntity player, String categoryKey) {
|
||||||
return switch (categoryKey) {
|
List<ContractDefinition> contracts = switch (categoryKey) {
|
||||||
case "mining" -> bundleSupplier.get().contractCatalog().contractsOfType(ContractType.MINING);
|
case "mining" -> bundleSupplier.get().contractCatalog().contractsOfType(ContractType.MINING);
|
||||||
case "hunting" -> bundleSupplier.get().contractCatalog().contractsOfType(ContractType.HUNTING);
|
case "hunting" -> bundleSupplier.get().contractCatalog().contractsOfType(ContractType.HUNTING);
|
||||||
default -> List.of();
|
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) {
|
private List<ContractDefinition> pagedContracts(List<ContractDefinition> contracts, int page) {
|
||||||
@@ -224,8 +250,8 @@ public final class ContractGuiService {
|
|||||||
return contracts.subList(from, to);
|
return contracts.subList(from, to);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int totalPages(String categoryKey) {
|
private int totalPages(ServerPlayerEntity player, String categoryKey) {
|
||||||
return Math.max(1, (int) Math.ceil(contractsFor(categoryKey).size() / (double) ITEM_SLOT_COUNT));
|
return Math.max(1, (int) Math.ceil(contractsFor(player, categoryKey).size() / (double) ITEM_SLOT_COUNT));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String categoryLabel(String categoryKey) {
|
private String categoryLabel(String categoryKey) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public final class SoulStealData {
|
|||||||
private Map<String, Boolean> scoreboardVisibility = new HashMap<>();
|
private Map<String, Boolean> scoreboardVisibility = new HashMap<>();
|
||||||
private Map<String, String> selectedContracts = new HashMap<>();
|
private Map<String, String> selectedContracts = new HashMap<>();
|
||||||
private Map<String, Map<String, Long>> contractProgress = new HashMap<>();
|
private Map<String, Map<String, Long>> contractProgress = new HashMap<>();
|
||||||
|
private Map<String, Set<String>> completedContracts = new HashMap<>();
|
||||||
|
|
||||||
public SoulStealData normalize() {
|
public SoulStealData normalize() {
|
||||||
if (souls == null) {
|
if (souls == null) {
|
||||||
@@ -56,6 +57,9 @@ public final class SoulStealData {
|
|||||||
if (contractProgress == null) {
|
if (contractProgress == null) {
|
||||||
contractProgress = new HashMap<>();
|
contractProgress = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
if (completedContracts == null) {
|
||||||
|
completedContracts = new HashMap<>();
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,4 +142,8 @@ public final class SoulStealData {
|
|||||||
public Map<String, Map<String, Long>> contractProgress() {
|
public Map<String, Map<String, Long>> contractProgress() {
|
||||||
return contractProgress;
|
return contractProgress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, Set<String>> completedContracts() {
|
||||||
|
return completedContracts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ public final class ContractService {
|
|||||||
if (contract.isEmpty()) {
|
if (contract.isEmpty()) {
|
||||||
return false;
|
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().selectedContracts().put(key(player.getUuid()), contract.get().id());
|
||||||
dataStore.data().contractProgress().remove(key(player.getUuid()));
|
dataStore.data().contractProgress().remove(key(player.getUuid()));
|
||||||
saveQuietly();
|
saveQuietly();
|
||||||
@@ -60,6 +63,12 @@ public final class ContractService {
|
|||||||
.getOrDefault(contractId, 0L);
|
.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) {
|
public void recordMining(ServerPlayerEntity player, String blockId) {
|
||||||
record(player, ContractType.MINING, blockId);
|
record(player, ContractType.MINING, blockId);
|
||||||
}
|
}
|
||||||
@@ -86,6 +95,11 @@ public final class ContractService {
|
|||||||
soulService.addSouls(player.getUuid(), contract.reward());
|
soulService.addSouls(player.getUuid(), contract.reward());
|
||||||
player.sendMessage(net.minecraft.text.Text.literal("Contract complete: " + contract.name() + " (+"
|
player.sendMessage(net.minecraft.text.Text.literal("Contract complete: " + contract.name() + " (+"
|
||||||
+ contract.reward() + " souls)").formatted(net.minecraft.util.Formatting.GREEN), false);
|
+ 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()) {
|
if (!contract.repeatable()) {
|
||||||
dataStore.data().selectedContracts().remove(playerKey);
|
dataStore.data().selectedContracts().remove(playerKey);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import net.minecraft.scoreboard.number.BlankNumberFormat;
|
|||||||
import net.minecraft.server.MinecraftServer;
|
import net.minecraft.server.MinecraftServer;
|
||||||
import net.minecraft.server.network.ServerPlayerEntity;
|
import net.minecraft.server.network.ServerPlayerEntity;
|
||||||
import net.minecraft.text.Text;
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.util.Formatting;
|
||||||
|
|
||||||
/** Owns toggleable HUD state, player names, leaderboard data, and wanted-player bossbars. */
|
/** Owns toggleable HUD state, player names, leaderboard data, and wanted-player bossbars. */
|
||||||
public final class HudService {
|
public final class HudService {
|
||||||
@@ -241,6 +242,16 @@ public final class HudService {
|
|||||||
|
|
||||||
private List<Text> buildSidebarLines(ServerPlayerEntity player, long nowEpochMillis) {
|
private List<Text> buildSidebarLines(ServerPlayerEntity player, long nowEpochMillis) {
|
||||||
List<StoredBounty> activeBounties = bountyService.activeBountiesForTarget(player.getUuid());
|
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 totalValue = activeBounties.stream().mapToLong(StoredBounty::soulValue).sum();
|
||||||
long remainingSeconds = activeBounties.stream()
|
long remainingSeconds = activeBounties.stream()
|
||||||
.mapToLong(StoredBounty::expiresAtEpochMillis)
|
.mapToLong(StoredBounty::expiresAtEpochMillis)
|
||||||
@@ -248,18 +259,12 @@ public final class HudService {
|
|||||||
.orElse(nowEpochMillis);
|
.orElse(nowEpochMillis);
|
||||||
remainingSeconds = Math.max(0L, (remainingSeconds - nowEpochMillis + 999L) / 1000L);
|
remainingSeconds = Math.max(0L, (remainingSeconds - nowEpochMillis + 999L) / 1000L);
|
||||||
|
|
||||||
return List.of(
|
lines.add(Text.literal("Bounties: " + activeBounties.size()).formatted(Formatting.RED));
|
||||||
Text.literal("Souls: " + soulService.balanceOf(player.getUuid())),
|
lines.add(Text.literal("Wanted Value: " + totalValue).formatted(Formatting.GOLD));
|
||||||
Text.literal(contractService.selectedContract(player.getUuid())
|
lines.add(Text.literal("Wanted Time: " + DurationFormatter.formatSeconds(remainingSeconds)).formatted(Formatting.DARK_RED));
|
||||||
.map(contract -> "Contract: " + contract.name())
|
}
|
||||||
.orElse("Contract: None")),
|
|
||||||
Text.literal(contractService.selectedContract(player.getUuid())
|
return lines;
|
||||||
.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 void clearSidebar(ServerPlayerEntity player) {
|
private void clearSidebar(ServerPlayerEntity player) {
|
||||||
@@ -289,7 +294,7 @@ public final class HudService {
|
|||||||
return scoreboard.addObjective(
|
return scoreboard.addObjective(
|
||||||
objectiveName,
|
objectiveName,
|
||||||
ScoreboardCriterion.DUMMY,
|
ScoreboardCriterion.DUMMY,
|
||||||
Text.literal(configSupplier.get().hud().scoreboard().title()),
|
Text.literal(configSupplier.get().hud().scoreboard().title()).formatted(Formatting.DARK_AQUA),
|
||||||
ScoreboardCriterion.RenderType.INTEGER,
|
ScoreboardCriterion.RenderType.INTEGER,
|
||||||
false,
|
false,
|
||||||
BlankNumberFormat.INSTANCE
|
BlankNumberFormat.INSTANCE
|
||||||
|
|||||||
Reference in New Issue
Block a user