From 39f75f83221718396c5c8ee554f994d46add0af0 Mon Sep 17 00:00:00 2001 From: kuroppoi <68156848+kuroppoi@users.noreply.github.com> Date: Fri, 23 Feb 2024 23:46:27 +0100 Subject: [PATCH] Player appearance rework --- .../gameserver/entity/EntityConfig.java | 5 ++ .../brainwine/gameserver/entity/npc/Npc.java | 6 ++ .../gameserver/entity/player/Appearance.java | 87 +++++++++++++++++++ .../entity/player/AppearanceSlot.java | 61 +++++++++++++ .../entity/player/ClothingSlot.java | 42 --------- .../gameserver/entity/player/ColorSlot.java | 32 ------- .../gameserver/entity/player/Player.java | 55 +++--------- .../entity/player/PlayerConfigFile.java | 15 +--- .../java/brainwine/gameserver/item/Item.java | 12 +++ .../gameserver/item/ItemRegistry.java | 16 ++++ .../requests/ChangeAppearanceRequest.java | 81 ++++++++++------- 11 files changed, 254 insertions(+), 158 deletions(-) create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/player/Appearance.java create mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/player/AppearanceSlot.java delete mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/player/ClothingSlot.java delete mode 100644 gameserver/src/main/java/brainwine/gameserver/entity/player/ColorSlot.java diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java index 0da885c..31d21be 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/EntityConfig.java @@ -30,6 +30,7 @@ public class EntityConfig { private float maxHealth = Entity.DEFAULT_HEALTH; private float baseSpeed = 3; private boolean character; + private boolean human; private boolean named; private Vector2i size = new Vector2i(1, 1); private EntityGroup group = EntityGroup.NONE; @@ -85,6 +86,10 @@ public class EntityConfig { return character; } + public boolean isHuman() { + return human; + } + public boolean isNamed() { return named; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java index 1b93189..db53dba 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/npc/Npc.java @@ -17,6 +17,7 @@ import brainwine.gameserver.entity.EntityLoot; import brainwine.gameserver.entity.EntityRegistry; import brainwine.gameserver.entity.FacingDirection; import brainwine.gameserver.entity.npc.behavior.SequenceBehavior; +import brainwine.gameserver.entity.player.Appearance; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.DamageType; import brainwine.gameserver.item.Item; @@ -104,6 +105,11 @@ public class Npc extends Entity { this.name = Naming.getRandomEntityName(); } + // Generate random appearance + if(config.isHuman()) { + properties.putAll(Appearance.getRandomAppearance()); + } + this.config = config; this.typeName = config.getName(); this.type = config.getType(); diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Appearance.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Appearance.java new file mode 100644 index 0000000..34684de --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Appearance.java @@ -0,0 +1,87 @@ +package brainwine.gameserver.entity.player; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import brainwine.gameserver.GameConfiguration; +import brainwine.gameserver.item.Item; +import brainwine.gameserver.item.ItemRegistry; +import brainwine.gameserver.util.MapHelper; + +/** + * Utility class for player appearance related stuff. + * Ghosts also have a random appearance, which is why it's here instead of in the player class. + */ +public class Appearance { + + public static Map getRandomAppearance() { + return getRandomAppearance(null); + } + + public static Map getRandomAppearance(Player player) { + Map appearance = new HashMap<>(); + + for(AppearanceSlot slot : AppearanceSlot.values()) { + // Skip if slot cannot be changed by players + if(!slot.isChangeable()) { + continue; + } + + String category = slot.getCategory(); + + // Color handling + if(slot.isColor()) { + List colors = getAvailableColors(slot, player); + + // Change appearance to random color + if(!colors.isEmpty()) { + appearance.put(slot.getId(), colors.get((int)(Math.random() * colors.size()))); + } + + continue; + } + + // Fetch list of items in this slot's category that the player owns + List items = ItemRegistry.getItemsByCategory(category).stream() + .filter(item -> item.isBase() || (player != null && player.getInventory().hasItem(item))) + .collect(Collectors.toList()); + + // Change appearance to random clothing item + if(!items.isEmpty()) { + appearance.put(slot.getId(), items.get((int)(Math.random() * items.size())).getCode()); + } + } + + return appearance; + } + + public static List getAvailableColors(AppearanceSlot slot) { + return getAvailableColors(null); + } + + public static List getAvailableColors(AppearanceSlot slot, Player player) { + List colors = new ArrayList<>(); + + // Return empty list if slot is not valid + if(!slot.isColor()) { + return colors; + } + + Map wardrobe = MapHelper.getMap(GameConfiguration.getBaseConfig(), "wardrobe", Collections.emptyMap()); + String category = slot.getCategory(); + + // Add base colors + colors.addAll(MapHelper.getList(wardrobe, category, Collections.emptyList())); + + // Add bonus colors + if(player != null && player.getInventory().hasItem(ItemRegistry.getItem("accessories/makeup"))) { + colors.addAll(MapHelper.getList(wardrobe, String.format("%s-bonus", category), Collections.emptyList())); + } + + return colors; + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/AppearanceSlot.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/AppearanceSlot.java new file mode 100644 index 0000000..43ed761 --- /dev/null +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/AppearanceSlot.java @@ -0,0 +1,61 @@ +package brainwine.gameserver.entity.player; + +public enum AppearanceSlot { + + SKIN_COLOR("c*", "skin-color", true), + HAIR_COLOR("h*", "hair-color", true), + HAIR("h", "hair", true), + FACIAL_HAIR("fh", "facialhair", true), + TOPS("t", "tops", true), + BOTTOMS("b", "bottoms", true), + FOOTWEAR("fw", "footwear", true), + HEADGEAR("hg", "headgear", true), + FACIAL_GEAR("fg", "facialgear", true), + FACIAL_GEAR_GLOW("fg*", "facialgear-glow"), + SUIT("u", "suit"), + TOPS_OVERLAY("to", "tops-overlay"), + TOPS_OVERLAY_GLOW("to*", "tops-overlay-glow"), + ARMS_OVERLAY("ao", "arms-overlay"), + LEGS_OVERLAY("lo", "legs-overlay"), + FOOTWEAR_OVERLAY("fo", "footwear-overlay"); + + private final String id; + private final String category; + private final boolean changeable; + + private AppearanceSlot(String id, String category) { + this(id, category, false); + } + + private AppearanceSlot(String id, String category, boolean changeable) { + this.id = id; + this.category = category; + this.changeable = changeable; + } + + public static AppearanceSlot fromId(String id) { + for(AppearanceSlot value : values()) { + if(value.getId().equals(id)) { + return value; + } + } + + return null; + } + + public String getId() { + return id; + } + + public String getCategory() { + return category; + } + + public boolean isChangeable() { + return changeable; + } + + public boolean isColor() { + return id.endsWith("*"); + } +} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/ClothingSlot.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/ClothingSlot.java deleted file mode 100644 index f0ab70b..0000000 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/ClothingSlot.java +++ /dev/null @@ -1,42 +0,0 @@ -package brainwine.gameserver.entity.player; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -public enum ClothingSlot { - - HAIR("h"), - FACIAL_HAIR("fh"), - TOPS("t"), - BOTTOMS("b"), - FOOTWEAR("fw"), - HEADGEAR("hg"), - FACIAL_GEAR("fg"), - SUIT("u"), - TOPS_OVERLAY("to"), - ARMS_OVERLAY("ao"), - LEGS_OVERLAY("lo"), - FOOTWEAR_OVERLAY("fo"); - - private final String id; - - private ClothingSlot(String id) { - this.id = id; - } - - @JsonCreator - public static ClothingSlot fromId(String id) { - for(ClothingSlot value : values()) { - if(value.getId().equals(id)) { - return value; - } - } - - return null; - } - - @JsonValue - public String getId() { - return id; - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/ColorSlot.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/ColorSlot.java deleted file mode 100644 index 8fe4caa..0000000 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/ColorSlot.java +++ /dev/null @@ -1,32 +0,0 @@ -package brainwine.gameserver.entity.player; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -public enum ColorSlot { - - SKIN_COLOR("c*"), - HAIR_COLOR("h*"); - - private final String id; - - private ColorSlot(String id) { - this.id = id; - } - - @JsonCreator - public static ColorSlot fromId(String id) { - for(ColorSlot value : values()) { - if(value.getId().equals(id)) { - return value; - } - } - - return null; - } - - @JsonValue - public String getId() { - return id; - } -} diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java index fc0b058..64edb0f 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/Player.java @@ -10,7 +10,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -103,8 +102,7 @@ public class Player extends Entity implements CommandExecutor { private Map ignoredHints; private Map skills; private Map> bumpedSkills; - private Map equippedClothing; - private Map equippedColors; + private Map appearance; private final Map settings = new HashMap<>(); private final Set activeChunks = new HashSet<>(); private final Map> dialogs = new HashMap<>(); @@ -147,8 +145,7 @@ public class Player extends Entity implements CommandExecutor { this.ignoredHints = config.getIgnoredHints(); this.skills = config.getSkills(); this.bumpedSkills = config.getBumpedSkills(); - this.equippedClothing = config.getEquippedClothing(); - this.equippedColors = config.getEquippedColors(); + this.appearance = config.getAppearance(); health = getMaxHealth(); inventory.setPlayer(this); statistics.setPlayer(this); @@ -169,8 +166,7 @@ public class Player extends Entity implements CommandExecutor { this.ignoredHints = new HashMap<>(); this.skills = new HashMap<>(); this.bumpedSkills = new HashMap<>(); - this.equippedClothing = new HashMap<>(); - this.equippedColors = new HashMap<>(); + this.appearance = Appearance.getRandomAppearance(); } @JsonCreator @@ -271,7 +267,8 @@ public class Player extends Entity implements CommandExecutor { public Map getStatusConfig() { Map config = super.getStatusConfig(); config.put("id", documentId); - config.putAll(getAppearanceConfig()); + config.putAll(appearance); + config.put("u", inventory.findJetpack().getCode()); return config; } @@ -1061,27 +1058,18 @@ public class Player extends Entity implements CommandExecutor { return Collections.unmodifiableSet(achievements); } - public void setClothing(ClothingSlot slot, Item item) { - if(!item.isClothing()) { - return; - } - - equippedClothing.put(slot, item); - zone.sendMessage(new EntityChangeMessage(id, getAppearanceConfig())); + public void randomizeAppearance() { + appearance.putAll(Appearance.getRandomAppearance(this)); + zone.sendMessage(new EntityChangeMessage(id, appearance)); } - public Map getEquippedClothing() { - return Collections.unmodifiableMap(equippedClothing); + public void updateAppearance(Map appearance) { + this.appearance = appearance; + zone.sendMessage(new EntityChangeMessage(id, appearance)); } - public void setColor(ColorSlot slot, String hex) { - // TODO check if the string is actually a valid hex color - equippedColors.put(slot, hex); - zone.sendMessage(new EntityChangeMessage(id, getAppearanceConfig())); - } - - public Map getEquippedColors() { - return Collections.unmodifiableMap(equippedColors); + public Map getAppearance() { + return Collections.unmodifiableMap(appearance); } public void setSkillLevel(Skill skill, int level) { @@ -1309,21 +1297,6 @@ public class Player extends Entity implements CommandExecutor { return connection != null && connection.isOpen(); } - private Map getAppearanceConfig() { - Map appearance = new HashMap<>(); - - for(Entry entry : equippedClothing.entrySet()) { - appearance.put(entry.getKey().getId(), entry.getValue().getCode()); - } - - for(Entry entry : equippedColors.entrySet()) { - appearance.put(entry.getKey().getId(), entry.getValue()); - } - - appearance.put(ClothingSlot.SUIT.getId(), inventory.findJetpack().getCode()); // Jetpack - return appearance; - } - /** * @return A {@link Map} containing all the data necessary for use in {@link ConfigurationMessage}. */ @@ -1345,7 +1318,7 @@ public class Player extends Entity implements CommandExecutor { config.put("items_crafted", statistics.getTotalItemsCrafted()); config.put("play_time", (int)(statistics.getPlayTime())); config.put("deaths", statistics.getDeaths()); - config.put("appearance", getAppearanceConfig()); + config.put("appearance", appearance); config.put("settings", settings); return config; } diff --git a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java index e786466..03fac5d 100644 --- a/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java +++ b/gameserver/src/main/java/brainwine/gameserver/entity/player/PlayerConfigFile.java @@ -39,8 +39,7 @@ public class PlayerConfigFile { private Map ignoredHints = new HashMap<>(); private Map skills = new HashMap<>(); private Map> bumpedSkills = new HashMap<>(); - private Map equippedClothing = new HashMap<>(); - private Map equippedColors = new HashMap<>(); + private Map appearance = new HashMap<>(); public PlayerConfigFile(Player player) { this.name = player.getName(); @@ -63,8 +62,7 @@ public class PlayerConfigFile { this.ignoredHints = player.getIgnoredHints(); this.skills = player.getSkills(); this.bumpedSkills = player.getBumpedSkills(); - this.equippedClothing = player.getEquippedClothing(); - this.equippedColors = player.getEquippedColors(); + this.appearance = player.getAppearance(); } @JsonCreator @@ -163,12 +161,7 @@ public class PlayerConfigFile { } @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) - public Map getEquippedClothing() { - return equippedClothing; - } - - @JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP) - public Map getEquippedColors() { - return equippedColors; + public Map getAppearance() { + return appearance; } } diff --git a/gameserver/src/main/java/brainwine/gameserver/item/Item.java b/gameserver/src/main/java/brainwine/gameserver/item/Item.java index 6e3a7ec..605dcc8 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/Item.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/Item.java @@ -30,6 +30,9 @@ public class Item { @JsonProperty("code") private int code; + @JsonProperty("category") + private String category; + @JsonProperty("title") private String title; @@ -237,6 +240,15 @@ public class Item { return code; } + public String getCategory() { + if(category != null) { + return category; + } + + int index = id.indexOf('/'); + return index > 1 ? id.substring(0, index) : null; + } + public String getTitle() { return title; } diff --git a/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java b/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java index 4b82d2c..db35ed7 100644 --- a/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java +++ b/gameserver/src/main/java/brainwine/gameserver/item/ItemRegistry.java @@ -2,9 +2,11 @@ package brainwine.gameserver.item; import static brainwine.shared.LogMarkers.SERVER_MARKER; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.logging.log4j.LogManager; @@ -15,6 +17,7 @@ public class ItemRegistry { private static final Logger logger = LogManager.getLogger(); private static final Map items = new HashMap<>(); private static final Map itemsByCode = new HashMap<>(); + private static final Map> itemsByCategory = new HashMap<>(); // TODO maybe just move the registry stuff here public static void clear() { @@ -36,6 +39,15 @@ public class ItemRegistry { return false; } + String category = item.getCategory(); + List categorizedItems = itemsByCategory.get(category); + + if(categorizedItems == null) { + categorizedItems = new ArrayList<>(); + itemsByCategory.put(category, categorizedItems); + } + + categorizedItems.add(item); items.put(id, item); itemsByCode.put(code, item); return true; @@ -52,4 +64,8 @@ public class ItemRegistry { public static Collection getItems() { return Collections.unmodifiableCollection(items.values()); } + + public static List getItemsByCategory(String category) { + return Collections.unmodifiableList(itemsByCategory.getOrDefault(category, Collections.emptyList())); + } } diff --git a/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java b/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java index beb7de2..6c903af 100644 --- a/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java +++ b/gameserver/src/main/java/brainwine/gameserver/server/requests/ChangeAppearanceRequest.java @@ -5,29 +5,28 @@ import java.util.Map.Entry; import brainwine.gameserver.annotations.RequestInfo; import brainwine.gameserver.dialog.DialogHelper; -import brainwine.gameserver.entity.player.ClothingSlot; -import brainwine.gameserver.entity.player.ColorSlot; +import brainwine.gameserver.entity.player.Appearance; +import brainwine.gameserver.entity.player.AppearanceSlot; import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.item.Item; import brainwine.gameserver.item.ItemRegistry; import brainwine.gameserver.server.PlayerRequest; +import brainwine.gameserver.server.messages.EntityChangeMessage; +import brainwine.gameserver.util.MapHelper; -/** - * TODO we should actually check if the sent value is even compatible with the slot. - * We wouldn't want to allow players to equip pants for hats! - */ @RequestInfo(id = 22) public class ChangeAppearanceRequest extends PlayerRequest { - public Map data; + public Map appearance; @Override public void process(Player player) { - if(data.containsKey("meta")) { - String meta = "" + data.get("meta"); + // Handle special cases + if(appearance.containsKey("meta")) { + String meta = MapHelper.getString(appearance, "meta", ""); if(meta.equals("randomize")) { - player.notify("Sorry, you can't randomize your appearance yet."); + player.randomizeAppearance(); } else { player.showDialog(DialogHelper.getWardrobeDialog(meta)); } @@ -35,36 +34,54 @@ public class ChangeAppearanceRequest extends PlayerRequest { return; } - for(Entry entry : data.entrySet()) { - String key = entry.getKey(); + // Validate appearance data + for(Entry entry : appearance.entrySet()) { + AppearanceSlot slot = AppearanceSlot.fromId(entry.getKey()); Object value = entry.getValue(); - if(value instanceof Integer) { - ClothingSlot slot = ClothingSlot.fromId(key); - - if(slot == null) { - continue; - } - - Item item = ItemRegistry.getItem((int)value); - - if(!item.isBase() && !player.getInventory().hasItem(item)) { - player.notify("Sorry, but you do not own this."); + // Fail if slot is not valid + if(slot == null || !slot.isChangeable()) { + fail(player); + return; + } + + // Handle color data + if(slot.isColor()) { + // Fail if color value is not a string + if(!(value instanceof String)) { + fail(player); return; } - player.setClothing(slot, item); - } else if(value instanceof String) { - // TODO check if player owns color - ColorSlot slot = ColorSlot.fromId(key); - String color = (String)value; - - if(slot == null) { - continue; + // Fail if player doesn't own color + if(!Appearance.getAvailableColors(slot, player).contains((String)value)) { + fail(player); + return; } - player.setColor(slot, color); + continue; + } + + // Fail if item value is not an integer (item code) + if(!(value instanceof Integer)) { + fail(player); + return; + } + + Item item = ItemRegistry.getItem((int)value); + + // Do nothing if item isn't valid clothing or player doesn't own it + if(!item.isClothing() || !slot.getCategory().equals(item.getCategory()) || (!item.isBase() && !player.getInventory().hasItem(item))) { + fail(player); + return; } } + + // Update player appearance + player.updateAppearance(appearance); + } + + private void fail(Player player) { + player.sendMessage(new EntityChangeMessage(player.getId(), player.getAppearance())); } }