Java tutorial
/* * CrimsonGlow is an adult computer roleplaying game with spanking content. * Copyright (C) 2015 Andrew Russell * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.spankingrpgs.model.characters; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import com.spankingrpgs.model.GameState; import com.spankingrpgs.model.combat.CombatRange; import com.spankingrpgs.model.skills.Skill; import com.spankingrpgs.model.items.Equipment; import com.spankingrpgs.model.story.EventDescription; import com.spankingrpgs.util.CollectionUtils; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Models the characters that appear in the game. There is only one thing that implementers need * to implement: A method for syncing up primary statistics with secondary statistics that * gets applied whenever a primary statistic is incremented or decremented. */ public abstract class GameCharacter { private final static Logger LOG = Logger.getLogger(GameCharacter.class.getCanonicalName()); /** * The character's name. This should be a unique identifier for the character, as it is the only value used to * test equality between characters. */ private String name; /** * The names printed to the screen for the player to see. This does not need to be unique. There should be * one name for each gender, if the player is allowed to select the character's gender. */ private Map<Gender, String> printedNames; /** * The name to display while in battle. */ private String battleName; private Map<String, AppearanceElement> appearance; private String description; private Gender gender; /** * The character's primary statistics. Primary statistics are those whose values are only affected by equipment. * Other statistics have no impact on them. */ private LinkedHashMap<String, Integer> primaryStatistics; /** * The character's secondary statistics. Secondary statistics are affected partially or completely by the value * of primary statistics. */ private LinkedHashMap<String, Integer> secondaryStatistics; /** * The equippable slots of the character. Guaranteed a consistent iteration order based on the order slots appear * in the map. */ private LinkedHashMap<String, EquipSlot> equipSlots; /** * A mapping from a status that the character is inflicted with, to the number of rounds remaining before the * status fades on its own */ private Map<Status, Integer> statuses; /** * Character-specific in-combat spanking text. The key is some situation, and the value is the resulting text * (for example, the key may be the name of a position, or the concatentation of a position and the string * "continuation" to key into the text for the second round of a spanking) */ private Map<String, EventDescription> spankingEvents; /** * A mapping from the names of the skills the character knows to their levels. */ private LinkedHashMap<String, Integer> skills; /** * The list of ranges this character can attack at. */ private List<CombatRange> attackRanges; private List<String> bumStatus; private SpankingRole role; /** * Constructor. * * @param name The name of the character * @param printedNames The name that is actually displayed to the player * @param battleName The battle name of the character * @param description A description of the character * @param gender The gender of the character * @param attackRanges The ranges at which the character can attack * @param primaryStatistics The primary statistics of the character * @param secondaryStatistics The starting values of the secondary statistics of the character * @param appearance The character's appearance * @param equipSlots The character's equipSlots * @param skills The character's skills * @param spankingEvents The in-combat spanking events for this character * @param role The role this character typically takes in a spanking */ public GameCharacter(String name, Map<Gender, String> printedNames, String battleName, String description, Gender gender, List<CombatRange> attackRanges, LinkedHashMap<String, Integer> primaryStatistics, LinkedHashMap<String, Integer> secondaryStatistics, Map<String, AppearanceElement> appearance, LinkedHashMap<String, EquipSlot> equipSlots, Map<Skill, Integer> skills, Map<String, EventDescription> spankingEvents, SpankingRole role) { this.name = name; this.printedNames = printedNames; this.battleName = battleName; this.description = description; this.gender = gender; this.attackRanges = attackRanges; this.primaryStatistics = primaryStatistics; this.secondaryStatistics = secondaryStatistics; this.appearance = appearance; this.equipSlots = equipSlots; this.statuses = new LinkedHashMap<>(); this.spankingEvents = spankingEvents; this.bumStatus = new LinkedList<>(); this.skills = new LinkedHashMap<>(); for (Map.Entry<Skill, Integer> skillEntry : skills.entrySet()) { this.skills.put(skillEntry.getKey().getName(), skillEntry.getValue()); } this.role = role; LOG.log(Level.INFO, String.format( "GameCharacter %s constructed: printedName: %s, statistics: %s, " + "appearance: %s, equipSlots: %s, statuses: %s, spankingRole: %s", this.name, this.printedNames, this.primaryStatistics, this.appearance, this.equipSlots, this.statuses, this.role)); } /** * Constructor. Skills default to an empty map * * @param name The name of the character * @param printedNames The name that is actually displayed to the player * @param battleName The battle name of the character * @param description A description of the character * @param gender The gender of the character * @param attackRanges The ranges at which the character can attack * @param primaryStatistics The primary statistics of the character * @param secondaryStatistics The starting values of the secondary statistics of the character * @param appearance The character's appearance * @param equipSlots The character's equipSlots * @param spankingEvents The in-combat spanking events for this character * @param role The role this charactr takes in a spanking */ public GameCharacter(String name, Map<Gender, String> printedNames, String battleName, String description, Gender gender, List<CombatRange> attackRanges, LinkedHashMap<String, Integer> primaryStatistics, LinkedHashMap<String, Integer> secondaryStatistics, Map<String, AppearanceElement> appearance, LinkedHashMap<String, EquipSlot> equipSlots, Map<String, EventDescription> spankingEvents, SpankingRole role) { this(name, printedNames, battleName, description, gender, attackRanges, primaryStatistics, secondaryStatistics, appearance, equipSlots, new LinkedHashMap<>(), spankingEvents, role); } /** * This constructor will compute the values of the secondary statistics using the modifySecondaryStatistics * method. * <p> * This constructor is best used when initializing a plain character, without any of their own special bonuses. * @param name The name of the character * @param printedNames The name to be displayed to the player * @param battleName The battle name of the character * @param description The description of the character * @param gender The gender of the character * @param attackRanges The ranges at which the character can attack * @param primaryStatistics The character's primary statistics * @param secondaryStatisticNames The names of the secondary statistics to be computed * @param appearance The character's appearance * @param equipSlots The slots into which the character can put equipment * @param spankingEvents This character's in-combat spanking text * @param role The role in a spanking that this character typically takes */ public GameCharacter(String name, Map<Gender, String> printedNames, String battleName, String description, Gender gender, List<CombatRange> attackRanges, LinkedHashMap<String, Integer> primaryStatistics, List<String> secondaryStatisticNames, Map<String, AppearanceElement> appearance, LinkedHashMap<String, EquipSlot> equipSlots, Map<String, EventDescription> spankingEvents, SpankingRole role) { this(name, printedNames, battleName, description, gender, attackRanges, primaryStatistics, secondaryStatisticNames, appearance, equipSlots, new LinkedHashMap<>(), spankingEvents, role); } /** * This constructor will compute the values of the secondary statistics using the modifySecondaryStatistics * method. * <p> * This constructor is best used when initializing a plain character, without any of their own special bonuses. * * @param name The name of the character * @param printedNames The name to be displayed to the player * @param battleName The battle name of the character * @param description The description of the character * @param gender The gender of the character * @param attackRanges The ranges at which the character can attack * @param primaryStatistics The character's primary statistics * @param secondaryStatisticNames The names of the secondary statistics to be computed * @param appearance The character's appearance * @param equipSlots The slots into which the character can put equipment * @param skills The character's skills * @param spankingEvents This character's in-combat spanking text * @param role The role this character tends to take in spankings */ public GameCharacter(String name, Map<Gender, String> printedNames, String battleName, String description, Gender gender, List<CombatRange> attackRanges, LinkedHashMap<String, Integer> primaryStatistics, List<String> secondaryStatisticNames, Map<String, AppearanceElement> appearance, LinkedHashMap<String, EquipSlot> equipSlots, Map<Skill, Integer> skills, Map<String, EventDescription> spankingEvents, SpankingRole role) { this(name, printedNames, battleName, description, gender, attackRanges, primaryStatistics, CollectionUtils.buildMap(secondaryStatisticNames.stream(), () -> 0), appearance, equipSlots, skills, spankingEvents, role); primaryStatistics.keySet().forEach(this::modifySecondaryStatistics); } /** * Performs a deep copy of the specified character. * * @param gameCharacter The character to be copied */ public GameCharacter(GameCharacter gameCharacter) { this.name = gameCharacter.name; this.printedNames = gameCharacter.printedNames.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); this.battleName = gameCharacter.battleName; this.description = gameCharacter.description; this.gender = gameCharacter.gender; this.attackRanges = gameCharacter.attackRanges.stream().map(CombatRange::copy).collect(Collectors.toList()); this.primaryStatistics = new LinkedHashMap<>(gameCharacter.primaryStatistics.size()); this.primaryStatistics.putAll(gameCharacter.primaryStatistics); this.secondaryStatistics = new LinkedHashMap<>(gameCharacter.secondaryStatistics.size()); this.secondaryStatistics.putAll(gameCharacter.secondaryStatistics); this.equipSlots = new LinkedHashMap<>(gameCharacter.equipSlots.size()); this.equipSlots.putAll(gameCharacter.equipSlots.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> new EquipSlot(entry.getValue())))); this.skills = gameCharacter.skills.entrySet().stream() .collect(CollectionUtils.toLinkedHashMap(Map.Entry::getKey, Map.Entry::getValue)); //Since AppearanceElements are immutable, a shallow copy is sufficient. this.appearance = gameCharacter.appearance.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); //Since statuses are immutable, a shallow copy is sufficient this.statuses = gameCharacter.statuses.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); this.spankingEvents = gameCharacter.spankingEvents.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); this.bumStatus = gameCharacter.bumStatus.stream().collect(Collectors.toList()); this.role = gameCharacter.getRole(); } /** * Returns a deep copy of this GameCharacter * * @return A deep copy of this GameCharacter */ public abstract GameCharacter copy(); /** * Given a JSON Object containing character state, replaces this character's state with the state stored in the * JSON object. * * @param characterObject The JSON Object containing the state to load */ public abstract void load(JsonNode characterObject); /** * Syncs up the values of the secondary statistics, with the new value of the primary statistics. This method * will recompute the values of all secondary statistics affected by the modified primary statistic. * * @param primaryStatisticName The name of the primary statistic that has been modified */ protected abstract void modifySecondaryStatistics(String primaryStatisticName); /** * Get the value of the specified statistic. * * @param statisticName The name of the statistic to get the value of * * @return The value of the statistic * * @throws IllegalArgumentException if this character does not have a statistic with name {@code statisticName}. */ @JsonIgnore public int getStatistic(String statisticName) { Integer primaryStatistic = primaryStatistics.get(statisticName); if (primaryStatistic != null) { return primaryStatistic; } return CollectionUtils.getValue(secondaryStatistics, statisticName); } /** * Returns the value of all the statistics in the order they were added to the character as an unmodifiable * map. * * @return the value of all the statistics in the order they were added to the character. */ @JsonIgnore public Map<String, Integer> getAllStatistics() { LinkedHashMap<String, Integer> allStatistics = new LinkedHashMap<>( primaryStatistics.size() + secondaryStatistics.size()); Stream.concat(primaryStatistics.entrySet().stream(), secondaryStatistics.entrySet().stream()) .forEach(entry -> allStatistics.put(entry.getKey(), entry.getValue())); return Collections.unmodifiableMap(allStatistics); } /** * Returns an unmodifiable map describing the character's primary statistics. * * @return An unmodifiable map describing the character's primary statistics */ @JsonProperty("primaryStatistics") public Map<String, Integer> getAllPrimaryStatistics() { return Collections.unmodifiableMap(primaryStatistics); } @JsonProperty("secondaryStatistics") public abstract Map<String, Integer> getSerializedSecondaryStatistics(); @JsonIgnore public Map<String, Integer> getAllSecondaryStatistics() { return Collections.unmodifiableMap(secondaryStatistics); } /** * Increments the specified statistic by the specified amount. * This method will not allow the specified statistic to fall below 0 if it is not already below 0. * * @param statisticName The name of the statistic to increment * @param amount The amount to increment the statistic by. * @throws IllegalArgumentException if this character does not have a statistic with name {@code statisticName}. */ public void incrementStatistic(String statisticName, int amount) { incrementStatistic(statisticName, amount, 0); } /** * Increments the specified statistic by the specified amount. * This method will not allow the specified statistic to fall below {@code min} if it is not already below it. * * @param statisticName The name of the statistic to increment * @param amount The amount to increment the statistic by. * @param min The minimum value allowed for this statistic * @throws IllegalArgumentException if this character does not have a statistic with name {@code statisticName}. */ public void incrementStatistic(String statisticName, int amount, int min) { int currentValue = getStatistic(statisticName); int flooredAmount = currentValue >= min && currentValue + amount < min ? 0 - currentValue : amount; setStatistic(statisticName, getStatistic(statisticName) + flooredAmount); } /** * Decrements the specified statistic by the specified amount. * Does not allow the statistic to fall below 0 if the value is not already below 0. * <p> * Note that after this method is invoked, the decremented statistic may be negative. * * @param statisticName The name of the statistic to decrement * @param amount The amount to decrement the statistic by. * * @throws IllegalArgumentException if this character does not have a statistic with name {@code statisticName}. */ public void decrementStatistic(String statisticName, int amount) { decrementStatistic(statisticName, amount, 0); } /** * Decrements the specified statistic by the specified amount. * Does not allow the statistic to fall below {@code min} if the value is not already below {@code min}. * <p> * Note that after this method is invoked, the decremented statistic may be negative. * * @param statisticName The name of the statistic to decrement * @param amount The amount to decrement the statistic by. * @param min The minimum value this statistic is allowed to reach * * @throws IllegalArgumentException if this character does not have a statistic with name {@code statisticName}. */ public void decrementStatistic(String statisticName, int amount, int min) { incrementStatistic(statisticName, 0 - amount, min); } /** * Given the name of a statistic, primary or secondary, sets the value of that statistic to the specified amount. * If the modified statistic is primary, then this method will also invoke * {@link GameCharacter#modifySecondaryStatistics(String)} on the name of the modified statistic. * * @param statisticName The name of the statistic to be set * @param value The new value of the statistic * * @throws IllegalArgumentException if this character does not have a statistic with name {@code statisticName}. */ public void setStatistic(String statisticName, int value) { if (primaryStatistics.containsKey(statisticName)) { primaryStatistics.put(statisticName, value); modifySecondaryStatistics(statisticName); } else if (secondaryStatistics.containsKey(statisticName)) { secondaryStatistics.put(statisticName, value); } else { String msg = String.format("%s is not a valid key for %s", statisticName, getAllStatistics()); LOG.log(Level.SEVERE, msg); throw new IllegalArgumentException(msg); } } /** * Equips an item. * * Also unequips anything that occupies at least one of the same slots as the item to be equipped. * * @param item The item to be equipped. * * @return The items unequipped as a side-effect of equipping {@code item}. */ public Set<Equipment> equip(Equipment item) { //Equip item, and get all the items that are displaced. List<String> newlyOccupiedSlotNames = item.getEquipSlotNames(); Set<Equipment> unequippedItems = newlyOccupiedSlotNames.stream() .map(slotName -> CollectionUtils.getValue(equipSlots, slotName)).map(slot -> slot.equip(item)) .filter(Optional::isPresent).map(Optional::get).collect(Collectors.toSet()); //Fully unequip displaced items. unequippedItems.stream().flatMap(unequippedItem -> unequippedItem.getEquipSlotNames().stream()) .filter(slotName -> !newlyOccupiedSlotNames.contains(slotName)) .map(slotName -> CollectionUtils.getValue(equipSlots, slotName)).forEach(EquipSlot::unequip); modifySecondaryStatisticsOnEquip(item); return unequippedItems; } /** * This method is invoked every time an item is equipped, and modifies the character's secondary statistics based * on the properties of the item being equipped. * * @param item The item being equipped */ protected abstract void modifySecondaryStatisticsOnEquip(Equipment item); /** * Unequips the item in the specified equip slot. * * @param slotName The name of the slot whose item is being unequipped * @return The item that was previously in this slot */ public Optional<Equipment> unequip(String slotName) { Optional<Equipment> unequippedItem = CollectionUtils.getValue(equipSlots, slotName).unequip(); if (unequippedItem.isPresent()) { modifySecondaryStatisticsOnUnEquip(unequippedItem.get()); } return unequippedItem; } /** * This method is invoked every time an item is unequipped, and modifies the character's secondary statistics * based on the properites of the item being unequipped. * * @param item The item being unequipped */ protected abstract void modifySecondaryStatisticsOnUnEquip(Equipment item); /** * Returns the equipment in the specified slot. * * @param slotName The name of the slot whose equipment is desired * @return The equipment in that slot */ @JsonIgnore public Optional<Equipment> getEquipment(String slotName) { return CollectionUtils.getValue(equipSlots, slotName).getEquipped(); } /** * Returns the names of all the equipment currently equipped by this character. * * @return The set of names of all the equipment currently equipped by this character */ public Set<String> getEquipmentNames() { return equipSlots.values().stream().map(EquipSlot::getEquipped).filter(Optional::isPresent) .map(Optional::get).map(Equipment::getName).collect(Collectors.toSet()); } public Set<Equipment> getEquipment() { return equipSlots.values().stream().map(EquipSlot::getEquipped).filter(Optional::isPresent) .map(Optional::get).collect(Collectors.toSet()); } @JsonIgnore public Map<String, EquipSlot> getEquipSlots() { return Collections.unmodifiableMap(equipSlots); } /** * Checks if the specified slot has any equipment. * * @param slotName The name of the slot of interest * @return True if the slot has equipment in it, false otherwise */ public boolean hasEquipment(String slotName) { return CollectionUtils.getValue(equipSlots, slotName).hasEquipment(); } public boolean isFemale() { return gender == Gender.FEMALE; } public boolean hasAGender() { return gender != null && gender != Gender.UNKNOWN; } public String getName() { return name; } public void setName(String name) { this.name = name; } @JsonIgnore public String getPrintedName() { return printedNames.get(gender); } /** * Returns an unmodifiable map of this character's male and female printed names. * * @return An unmodifiable map of this character's male and female printed names */ public Map<Gender, String> getBothPrintedNames() { return Collections.unmodifiableMap(printedNames); } public void setBothPrintedNames(Map<Gender, String> printedNames) { this.printedNames = printedNames; } public List<CombatRange> getAttackRanges() { return Collections.unmodifiableList(attackRanges); } /** * Returns a subset of the character's attackable ranges based on the character's position. * <p> * If the character is grappling, this returns either the singleton list {@link CombatRange#GRAPPLE} or an * empty list. Otherwise, the method returns all of the character's targetable ranges except for * {@link CombatRange#GRAPPLE}. * * @param characterPosition The character's current position * * @return A list of ranges the character can actually target from the specified position */ @JsonIgnore public List<CombatRange> getTargetableRanges(CombatRange characterPosition) { if (characterPosition == CombatRange.GRAPPLE) { return getAttackRanges().stream().filter(range -> range == characterPosition) .collect(Collectors.toList()); } return attackRanges.stream().filter(range -> range != CombatRange.GRAPPLE).collect(Collectors.toList()); } public void setAttackRanges(List<CombatRange> attackRanges) { this.attackRanges = attackRanges; } public Gender getGender() { return gender; } public void setGender(Gender gender) { this.gender = gender; } /** * Returns a description of the character. If no description was provided at construction, builds a simple * description based on the values in appearance * * @return A description of the character */ @JsonIgnore public String getDescription() { if (description == null) { return String.join("\n", appearance.entrySet().stream() .map(entry -> entry.getKey() + ": " + entry.getValue()).collect(Collectors.toList())); } else { return description; } } /** * Returns the actual value stored in {@code description} as opposed to {@link GameCharacter#getDescription()}, * which computes a description if description is null. * * @return This character's description, or null if this character has no description */ public String getRawDescription() { return description; } public void setDescription(String description) { this.description = description; } /** * Returns an unmodifiable view of this character's bum status. * * @return An unmodifiable view of this character's bum status */ public List<String> getBumStatus() { return Collections.unmodifiableList(bumStatus); } public void addBumStatus(String bumStatus) { this.bumStatus.add(bumStatus); } public void popBumStatus() { if (!bumStatus.isEmpty()) { bumStatus.remove(0); } } public void clearBumStatus() { bumStatus.clear(); } /** * Modify the character's appearance. * * @param appearanceElementName The appearance element to be changed * @param newAppearance The element to replace the old element */ public void changeAppearance(String appearanceElementName, AppearanceElement newAppearance) { appearance.put(appearanceElementName, newAppearance); } /** * Returns an adjective describing some aspect of the character, based on the desired adjective element. * * @return An adjective describing the character's bottom, based on the desired adjective element. */ @JsonIgnore public String getAdjective(String appearanceElementName) { return CollectionUtils.getValue(appearance, appearanceElementName).generateAdjective(); } /** * Returns an unmodifiable map describing the appearance of this character * * @return An unmodifiable map of this character's appearance elements */ public Map<String, AppearanceElement> getAppearance() { return Collections.unmodifiableMap(appearance); } /** * Given the name of an appearance element, returns the associated appearance element. * * @param appearanceName The name of the desired appearance element * * @return The desired appearance element */ public AppearanceElement getAppearance(String appearanceName) { return CollectionUtils.getValue(appearance, appearanceName); } /** * Inflict the specified status for the specified duration on the character. * <p> * If this character already has the specified status, the status with the longer duration is used. * * @param status The status to inflict * @param duration The duration of the status */ public void inflictStatus(Status status, int duration) { if (statuses.containsKey(status) && duration < statuses.get(status)) { return; } else if (statuses.containsKey(status)) { cureStatus(statuses.keySet().stream().filter(key -> key.equals(status)).findFirst().get()); } statuses.put(status, duration); status.applyStatus(this); } /** * Cures the character of the specified status. * If the character is not inflicted with the specified status, this method does nothing. * * @param status The status to cure the character of */ public void cureStatus(Status status) { if (statuses.containsKey(status)) { statuses.remove(status); status.reverseStatus(this); } } /** * Cure all the statuses currently inflicting this character */ public void cureAllStatuses() { Set<Status> statusSet = new HashSet<>(statuses.keySet()); statusSet.stream().forEach(this::cureStatus); } /** * Reduces the duration of the specified status by the specified amount. * If after the reduction, the duration of the status is 0 or less, the status is cured. If this character is * not afflicted with the specified status, this method does nothing. * * @param status The status whose duration should be reduced * @param reduction The amount to reduce the duration by */ public void decrementStatusDuration(Status status, int reduction) { if (statuses.containsKey(status)) { int duration = statuses.get(status) - reduction; if (duration <= 0) { cureStatus(status); } else { statuses.put(status, duration); } } } /** * Reduces the duration of all statuses on this character by the amount specified. * * @param reduction The amount to reduce the duration of each status by */ public void decrementAllStatusDurations(int reduction) { Set<Status> keys = new HashSet<>(statuses.keySet()); keys.stream().forEach(status -> decrementStatusDuration(status, reduction)); } /** * Returns an immutable map describing the statuses that inflict this character. * * @return An immutable map describing the statuses inflicting this character */ public Map<Status, Integer> getStatuses() { return Collections.unmodifiableMap(statuses); } public void setStatuses(Map<Status, Integer> statuses) { this.statuses = statuses; } /** * Determines if this character is inflicted with the specified status. * * @param status The status we're interested in * * @return True if this character is inflicted with the specified status. */ public boolean isInflictedWith(Status status) { return statuses.containsKey(status); } public void addSpankingEvent(String key, EventDescription value) { spankingEvents.put(key, value); } public void addSpankingEvents(Map<String, EventDescription> spankingEvents) { this.spankingEvents.putAll(spankingEvents); } /** * Returns the spanking text associated to the specified key. * * @param key The key of the text desired * @return The desired spanking text */ @JsonIgnore public Optional<EventDescription> getSpankingEvent(String key) { return Optional.ofNullable(spankingEvents.get(key)); } @JsonIgnore public Map<String, EventDescription> getSpankingEvents() { return Collections.unmodifiableMap(spankingEvents); } /** * Returns the level at which this character knows the specified skill, or an empty Optional if the character * does not know the skill. * * @param skill The skill whose level is desired * * @return The level at which this character knows the skill, or nothing if the character doesn't know the skill */ @JsonIgnore public OptionalInt getSkillLevel(Skill skill) { return knowsSkill(skill) ? OptionalInt.of(skills.get(skill.getName())) : OptionalInt.empty(); } /** * Learns the specified skill at level one. * * @param skill The skill to learn * @return The set of new skills whose prerequisites have been met */ public Set<Skill> learnSkill(Skill skill) { addSkill(skill.getName(), 1); return GameState.getInstance().getSkills().values().stream() .filter(newSkill -> newSkill.arePrerequisitesMet(this)).collect(Collectors.toSet()); } /** * Adds the specified skill to this character's repetoire at the specified level. * * @param skillName The name of the skill to be added * @param level The level at which to add the skill */ public void addSkill(String skillName, int level) { skills.put(skillName, level); } public void forgetSkill(String skillName) { skills.remove(skillName); } public void clearSkills() { skills.clear(); } /** * Improves the level of the specified skill by 1. * * @param skill The skill to improve by one level * * @return The set of names of new skills whose prerequisites have been met */ public Set<String> improveSkill(Skill skill) { skills.put(skill.getName(), skills.get(name) + 1); return GameState.getInstance().getSkills().values().stream() .filter(newSkill -> newSkill.arePrerequisitesMet(this)).map(Skill::getName) .collect(Collectors.toSet()); } public boolean knowsSkill(Skill skill) { return skills.containsKey(skill.getName()); } /** * Returns an unmodifiable map describing the skills this character knows, and the level at which they know them. * * @return An unmodifiable map describing the skills this character knows, and the level at which they know them. */ public Map<String, Integer> getSkills() { return Collections.unmodifiableMap(skills); } public void setSkills(LinkedHashMap<String, Integer> skills) { this.skills = skills; } public String getBattleName() { return battleName; } public void setBattleName(String battleName) { this.battleName = battleName; } @JsonIgnore public SpankingRole getRole() { return role; } /** * Equality based on the name of the character. Note: This equality does _not_ check class. Therefore, a * GameCharacter with name "Sue" and a GameCharacterChildClass with name "Sue" will be considered equal. * However, the two objects will _not_ be considered equal if the other object is not a subclass of GameCharacter. * * @param o The other GameCharacter being checked for equality * @return True if the two characters are equal (in the sense of having the same name) */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || !(o instanceof GameCharacter)) { return false; } GameCharacter that = (GameCharacter) o; return this.name.equals(that.name); } @Override public String toString() { return "GameCharacter{" + "name='" + name + '\'' + ", printedName='" + printedNames + '\'' + ", appearance=" + appearance + ", description='" + description + '\'' + ", primaryStatistics=" + primaryStatistics + ", secondaryStatistics=" + secondaryStatistics + ", equipSlots=" + equipSlots + ", statuses=" + statuses + '}'; } @Override public int hashCode() { return name != null ? name.hashCode() : 0; } }