diff --git a/mods/shop_categories.json b/mods/shop_categories.json index a631e7f..9984f5b 100644 --- a/mods/shop_categories.json +++ b/mods/shop_categories.json @@ -3,5 +3,10 @@ "id": 1, "name": "Consumables", "description": "Limited usage consumable items." + }, + { + "id": 2, + "name": "Services", + "description": "Non-item services and boosts." } -] \ No newline at end of file +] diff --git a/mods/shop_items.json b/mods/shop_items.json index ca3ea35..a73f71b 100644 --- a/mods/shop_items.json +++ b/mods/shop_items.json @@ -4,7 +4,33 @@ "categoryId": 1, "name": "Sleep Powder", "description": "Grants 1 hour of sleep bonus.", - "ironPrice": 50000, - "image": "https://www.wurmpedia.com/images/5/5e/Sleep_powder.png" + "image": "https://www.wurmpedia.com/images/5/5e/Sleep_powder.png", + "currency": { + "type": "WurmBankCurrency", + "ironAmount": 50000 + }, + "handler": { + "type": "ShopWurmItemPurchaseEffect", + "itemTemplateId": 740, + "ql": 99.0, + "rarity": 0 + } + }, + { + "id": 2, + "categoryId": 1, + "name": "Res Stone", + "description": "Gives you one resurrection charge.", + "image": "https://www.wurmpedia.com/images/a/aa/Resurrection_stone_icon.png", + "currency": { + "type": "WurmBankCurrency", + "ironAmount": 100000 + }, + "handler": { + "type": "ShopWurmItemPurchaseEffect", + "itemTemplateId": 302, + "ql": 99.0, + "rarity": 0 + } } -] \ No newline at end of file +] diff --git a/src/main/java/mod/treestar/shopmod/categoryprovider/JsonShopCategoryProvider.java b/src/main/java/mod/treestar/shopmod/categoryprovider/JsonShopCategoryProvider.java index 6b18bd4..d2fc2a4 100644 --- a/src/main/java/mod/treestar/shopmod/categoryprovider/JsonShopCategoryProvider.java +++ b/src/main/java/mod/treestar/shopmod/categoryprovider/JsonShopCategoryProvider.java @@ -1,18 +1,25 @@ package mod.treestar.shopmod.categoryprovider; import com.wurmonline.server.support.JSONArray; +import com.wurmonline.server.support.JSONObject; import com.wurmonline.server.support.JSONTokener; import mod.treestar.shopmod.datamodels.ShopCategory; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; public class JsonShopCategoryProvider implements ShopCategoryProvider { + private static final Logger logger = Logger.getLogger(JsonShopCategoryProvider.class.getName()); + private final String categoryJsonPath; + private long lastModified = -1L; + private List cachedCategories = Collections.emptyList(); public JsonShopCategoryProvider(String categoryJsonPath) { this.categoryJsonPath = categoryJsonPath; @@ -20,14 +27,46 @@ public class JsonShopCategoryProvider implements ShopCategoryProvider { @Override public List getCategories() { - return Collections.emptyList(); + try { + maybeReload(); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed loading categories from " + categoryJsonPath, e); + } + return cachedCategories; } - private void loadCategories() throws IOException { + private void maybeReload() throws IOException { File file = new File(categoryJsonPath); + if (!file.exists()) { + logger.log(Level.WARNING, "Category file not found at " + categoryJsonPath); + cachedCategories = Collections.emptyList(); + return; + } + if (file.lastModified() == lastModified) { + return; // up to date + } try (FileInputStream f = new FileInputStream(file)) { JSONTokener tokenizer = new JSONTokener(f); JSONArray typeArray = new JSONArray(tokenizer); + cachedCategories = parseCategories(typeArray); + lastModified = file.lastModified(); } } + + private List parseCategories(JSONArray typeArray) { + List categories = new ArrayList<>(); + for (int i = 0; i < typeArray.length(); i++) { + JSONObject obj = typeArray.getJSONObject(i); + if (!obj.has("id") || !obj.has("name")) { + logger.log(Level.WARNING, "Skipping category missing required fields at index " + i); + continue; + } + ShopCategory category = new ShopCategory(); + category.setId(obj.getInt("id")); + category.setName(obj.getString("name")); + category.setDescription(obj.optString("description", "")); + categories.add(category); + } + return categories; + } } diff --git a/src/main/java/mod/treestar/shopmod/datamodels/ShopItem.java b/src/main/java/mod/treestar/shopmod/datamodels/ShopItem.java index db4ae9c..422a923 100644 --- a/src/main/java/mod/treestar/shopmod/datamodels/ShopItem.java +++ b/src/main/java/mod/treestar/shopmod/datamodels/ShopItem.java @@ -72,4 +72,15 @@ public class ShopItem { public void setCurrency(ShopCurrency currency) { this.currency = currency; } + + /** + * Human-readable price for UI/BML display. + */ + public String getPriceDisplay() { + return currency != null ? currency.getDisplay() : "Unavailable"; + } + + public boolean hasCurrency() { + return currency != null; + } } diff --git a/src/main/java/mod/treestar/shopmod/itemprovider/JsonShopItemProvider.java b/src/main/java/mod/treestar/shopmod/itemprovider/JsonShopItemProvider.java index 49382f0..3f0ed10 100644 --- a/src/main/java/mod/treestar/shopmod/itemprovider/JsonShopItemProvider.java +++ b/src/main/java/mod/treestar/shopmod/itemprovider/JsonShopItemProvider.java @@ -1,20 +1,35 @@ package mod.treestar.shopmod.itemprovider; import com.wurmonline.server.support.JSONArray; +import com.wurmonline.server.support.JSONObject; import com.wurmonline.server.support.JSONTokener; +import mod.treestar.shopmod.currencies.ShopCurrency; +import mod.treestar.shopmod.currencies.WurmBankCurrency; import mod.treestar.shopmod.datamodels.ShopItem; +import mod.treestar.shopmod.purchasehandlers.ShopItemPurchaseEffect; +import mod.treestar.shopmod.purchasehandlers.ShopWurmItemPurchaseEffect; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; +import java.util.logging.Level; +import java.util.logging.Logger; /** - * Loads shop items from a JSON file. Parsing will be completed in a later step. + * Loads shop items from a JSON file. Expects an array of objects with: + * id (int), name (string), description (string, optional), image (string, optional), + * categoryId (int), currency (object with type and amount), handler (object describing purchase effect). */ public class JsonShopItemProvider implements ShopItemProvider { + private static final Logger logger = Logger.getLogger(JsonShopItemProvider.class.getName()); + private final String itemJsonPath; + private long lastModified = -1L; + private List cachedItems = Collections.emptyList(); public JsonShopItemProvider(String itemJsonPath) { this.itemJsonPath = itemJsonPath; @@ -22,14 +37,97 @@ public class JsonShopItemProvider implements ShopItemProvider { @Override public List getItems() { - return Collections.emptyList(); + try { + maybeReload(); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed loading items from " + itemJsonPath, e); + } + return cachedItems; } - private void loadItems() throws IOException { + private void maybeReload() throws IOException { File file = new File(itemJsonPath); + if (!file.exists()) { + logger.log(Level.WARNING, "Item file not found at " + itemJsonPath); + cachedItems = Collections.emptyList(); + return; + } + if (file.lastModified() == lastModified) { + return; // up to date + } try (FileInputStream f = new FileInputStream(file)) { JSONTokener tokenizer = new JSONTokener(f); JSONArray typeArray = new JSONArray(tokenizer); + cachedItems = parseItems(typeArray); + lastModified = file.lastModified(); + } + } + + private List parseItems(JSONArray typeArray) { + List items = new ArrayList<>(); + for (int i = 0; i < typeArray.length(); i++) { + JSONObject obj = typeArray.getJSONObject(i); + if (!obj.has("id") || !obj.has("name") || !obj.has("categoryId")) { + logger.log(Level.WARNING, "Skipping item missing required fields at index " + i); + continue; + } + ShopItem item = new ShopItem(); + item.setId(obj.getInt("id")); + item.setName(obj.getString("name")); + item.setDescription(obj.optString("description", "")); + item.setImage(obj.optString("image", "")); + item.setCategoryId(obj.getInt("categoryId")); + ShopCurrency currency = parseCurrency(obj.optJSONObject("currency"), i); + item.setCurrency(currency); + ShopItemPurchaseEffect handler = parsePurchaseHandler(obj.optJSONObject("handler"), i); + item.setPurchaseHandler(handler); + if (currency == null || handler == null) { + logger.log(Level.WARNING, "Skipping item at index " + i + " due to missing currency or handler"); + continue; + } + items.add(item); + } + return items; + } + + private ShopCurrency parseCurrency(JSONObject currencyObj, int index) { + if (currencyObj == null) { + logger.log(Level.WARNING, "Item at index " + index + " missing currency; item will be unusable"); + return null; + } + String type = currencyObj.optString("type", ""); + switch (type) { + case "WurmBankCurrency": + long ironAmount = currencyObj.optLong("ironAmount", currencyObj.optLong("priceIron", 0L)); + return new WurmBankCurrency(ironAmount); + default: + logger.log(Level.WARNING, "Unknown currency type '" + type + "' for item at index " + index); + return null; + } + } + + private ShopItemPurchaseEffect parsePurchaseHandler(JSONObject handlerObj, int index) { + if (handlerObj == null) { + logger.log(Level.WARNING, "Item at index " + index + " missing handler; item will be unusable"); + return null; + } + String type = handlerObj.optString("type", ""); + switch (type) { + case "ShopWurmItemPurchaseEffect": + int templateId = handlerObj.optInt("itemTemplateId", -1); + if (templateId <= 0) { + logger.log(Level.WARNING, "Handler for item at index " + index + " missing valid itemTemplateId"); + return null; + } + ShopWurmItemPurchaseEffect effect = new ShopWurmItemPurchaseEffect(); + effect.setItemTemplateId(templateId); + effect.setQl((float) handlerObj.optDouble("ql", 50)); + effect.setRandomQl(handlerObj.optBoolean("randomQl", false)); + effect.setRarity((byte) handlerObj.optInt("rarity", 0)); + return effect; + default: + logger.log(Level.WARNING, "Unknown handler type '" + type + "' for item at index " + index); + return null; } } } diff --git a/src/main/java/mod/treestar/shopmod/purchasehandlers/ShopWurmItemPurchaseEffect.java b/src/main/java/mod/treestar/shopmod/purchasehandlers/ShopWurmItemPurchaseEffect.java index ab78843..50820c6 100644 --- a/src/main/java/mod/treestar/shopmod/purchasehandlers/ShopWurmItemPurchaseEffect.java +++ b/src/main/java/mod/treestar/shopmod/purchasehandlers/ShopWurmItemPurchaseEffect.java @@ -5,8 +5,6 @@ import com.wurmonline.server.Server; import com.wurmonline.server.items.*; import com.wurmonline.server.players.Player; -import java.util.Random; - /** * Simple item purchase effect that gives a player a specific item on a successful purchase. * @see ShopItemPurchaseEffect @@ -17,6 +15,22 @@ public class ShopWurmItemPurchaseEffect implements ShopItemPurchaseEffect { private boolean randomQl; private byte rarity; + public void setItemTemplateId(int itemTemplateId) { + this.itemTemplateId = itemTemplateId; + } + + public void setQl(float ql) { + this.ql = ql; + } + + public void setRandomQl(boolean randomQl) { + this.randomQl = randomQl; + } + + public void setRarity(byte rarity) { + this.rarity = rarity; + } + @Override public void onPurchase(Player player) { try {