Player appearance rework

This commit is contained in:
kuroppoi 2024-02-23 23:46:27 +01:00
parent c5ff0ba62e
commit 39f75f8322
11 changed files with 254 additions and 158 deletions

View file

@ -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;
}

View file

@ -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();

View file

@ -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<String, Object> getRandomAppearance() {
return getRandomAppearance(null);
}
public static Map<String, Object> getRandomAppearance(Player player) {
Map<String, Object> 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<String> 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<Item> 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<String> getAvailableColors(AppearanceSlot slot) {
return getAvailableColors(null);
}
public static List<String> getAvailableColors(AppearanceSlot slot, Player player) {
List<String> colors = new ArrayList<>();
// Return empty list if slot is not valid
if(!slot.isColor()) {
return colors;
}
Map<String, Object> 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;
}
}

View file

@ -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("*");
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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<String, Float> ignoredHints;
private Map<Skill, Integer> skills;
private Map<Item, List<Skill>> bumpedSkills;
private Map<ClothingSlot, Item> equippedClothing;
private Map<ColorSlot, String> equippedColors;
private Map<String, Object> appearance;
private final Map<String, Object> settings = new HashMap<>();
private final Set<Integer> activeChunks = new HashSet<>();
private final Map<Integer, Consumer<Object[]>> 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<String, Object> getStatusConfig() {
Map<String, Object> 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<ClothingSlot, Item> getEquippedClothing() {
return Collections.unmodifiableMap(equippedClothing);
public void updateAppearance(Map<String, Object> 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<ColorSlot, String> getEquippedColors() {
return Collections.unmodifiableMap(equippedColors);
public Map<String, Object> 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<String, Object> getAppearanceConfig() {
Map<String, Object> appearance = new HashMap<>();
for(Entry<ClothingSlot, Item> entry : equippedClothing.entrySet()) {
appearance.put(entry.getKey().getId(), entry.getValue().getCode());
}
for(Entry<ColorSlot, String> 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;
}

View file

@ -39,8 +39,7 @@ public class PlayerConfigFile {
private Map<String, Float> ignoredHints = new HashMap<>();
private Map<Skill, Integer> skills = new HashMap<>();
private Map<Item, List<Skill>> bumpedSkills = new HashMap<>();
private Map<ClothingSlot, Item> equippedClothing = new HashMap<>();
private Map<ColorSlot, String> equippedColors = new HashMap<>();
private Map<String, Object> 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<ClothingSlot, Item> getEquippedClothing() {
return equippedClothing;
}
@JsonSetter(nulls = Nulls.SKIP, contentNulls = Nulls.SKIP)
public Map<ColorSlot, String> getEquippedColors() {
return equippedColors;
public Map<String, Object> getAppearance() {
return appearance;
}
}

View file

@ -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;
}

View file

@ -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<String, Item> items = new HashMap<>();
private static final Map<Integer, Item> itemsByCode = new HashMap<>();
private static final Map<String, List<Item>> 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<Item> 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<Item> getItems() {
return Collections.unmodifiableCollection(items.values());
}
public static List<Item> getItemsByCategory(String category) {
return Collections.unmodifiableList(itemsByCategory.getOrDefault(category, Collections.emptyList()));
}
}

View file

@ -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<String, Object> data;
public Map<String, Object> 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<String, Object> entry : data.entrySet()) {
String key = entry.getKey();
// Validate appearance data
for(Entry<String, Object> 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()));
}
}