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.scarletmoon.loader; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.spankingrpgs.scarletmoon.characters.PrimaryStatisticName; import com.spankingrpgs.scarletmoon.characters.SecondaryStatisticName; import com.spankingrpgs.scarletmoon.characters.appearance.BodyType; import com.spankingrpgs.scarletmoon.characters.appearance.EyeColor; import com.spankingrpgs.scarletmoon.characters.appearance.HairColor; import com.spankingrpgs.scarletmoon.characters.appearance.HairStyle; import com.spankingrpgs.scarletmoon.characters.appearance.Height; import com.spankingrpgs.scarletmoon.characters.appearance.Musculature; import com.spankingrpgs.scarletmoon.characters.appearance.SkinColor; import com.spankingrpgs.scarletmoon.items.CrimsonGlowEquipSlotNames; import com.spankingrpgs.model.GameState; import com.spankingrpgs.model.characters.AppearanceElement; import com.spankingrpgs.model.characters.CharacterFactory; import com.spankingrpgs.model.characters.EquipSlot; import com.spankingrpgs.model.characters.GameCharacter; import com.spankingrpgs.model.characters.Gender; import com.spankingrpgs.model.characters.SpankingRole; import com.spankingrpgs.model.combat.CombatRange; import com.spankingrpgs.model.items.Equipment; import com.spankingrpgs.model.loader.Loader; import com.spankingrpgs.model.skills.Skill; import com.spankingrpgs.model.story.EventDescription; import com.spankingrpgs.model.story.TextParser; import com.spankingrpgs.util.EnumUtils; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; /** * Loads character data from JSON files that mirror the structure of * {@link com.spankingrpgs.model.characters.GameCharacter}. So a character may look something like this: * * { * "name": "Jane Doe", * "printedName": { * "male": "Joe", * "female": "Jane" * }, * "battleName": "Crimson Glow", * "description": "Sample description", * "gender": "female", * "attackRanges": ["armslength"], * "primaryStatistics" : { * "strength": 1, * "speed": 1, * "willpower": 1 * }, * "secondaryStatistics": { * "maximum energy": 20 * }, * "experience": { * "strength": { * "tracker": "strengthTracker", * "experience": 0 * }, * "willpower": { * "tracker": "willpowerTracker", * "experience": 10 * }, * "speed": { * "tracker": "speedTracker", * "experience": 20 * } * }, * "appearance": { * "skinColor": "pale", * "height": "tall", * "musculature": "average", * "bodyType": "voluptuous", * "eyeColor": "blue", * "hairColor": "brown", * "hairStyle": "down" * }, * "equipment": { * "upper body": "t-shirt", * "lower body": "jeans", * "underwear": "thong" * }, * "skills": { * "Crimson Armor": 1, * "Crimson Slap": 2 * }, * "in combat spanking": { * "otk-start": "Start OTK spanking [name(spankee)]!", * "otk-continue": "Continue OTK spanking [name(spankee)]!", * } * } * * in YAML: * name: Jane Doe * printedName: * male: Joe * female: Jane * battleName: Crimson Glow * description: Sample description * gender: female * attackRanges: [armslength] * primaryStatistics: * strength: 1 * speed: 1 * willpower: 1 * secondaryStatistics: * maximum energy: 20 * experience: * strength: * tracker: strengthTracker * experience: 0 * willpower: * tracker: willpowerTracker * experience: 10 * speed: * tracker: speedTracker * experience: 20 * appearance: * skinColor: pale * height: tall * musculature: average * bodyType: voluptuous * eyeColor: blue * hairColor: brown * hairStyle: down * equipment: * upper body: t-shirt * lower body: jeans * underwear: thong * skills: * Crimson Armor: 1 * Crimson Slap: 2 * in combat spanking: * otk-start: | * Start OTK spanking [name(spankee)]! * otk-continue: | * Continue OTK spanking [name(spankee)]! * * Note that this class's load method should be invoked _after_ {@link ItemLoader}, because the CharacterLoader * assumes the items have already been loaded. */ public class CharacterLoader implements Loader { private static final Logger LOG = Logger.getLogger(CharacterLoader.class.getName()); private static final ObjectMapper JSON_PARSER = new ObjectMapper(); public static final String NAME = "name"; public static final String PRINTED_NAME = "printedName"; public static final String DESCRIPTION = "description"; public static final String BATTLE_NAME = "battleName"; public static final String GENDER = "gender"; public static final String ATTACK_RANGES = "attackRanges"; public static final String PRIMARY_STATISTICS = "primaryStatistics"; public static final String SECONDARY_STATISTICS = "secondaryStatistics"; public static final String EXPERIENCE = "experience"; public static final String APPEARANCE = "appearance"; public static final String EQUIPMENT = "equipment"; public static final String SKILLS = "skills"; public static final String MALE = "male"; public static final String FEMALE = "female"; public static final String ROLE = "role"; private final CharacterFactory characterFactory; private final ObjectMapper parser; /** * Builds an object that can turn file format supported by Jackson described in class docs into a * {@link GameCharacter}. * * @param characterFactory The factory to use to build the GameCharacter * @param parser The parser to use when parsing the data files with character data */ public CharacterLoader(CharacterFactory characterFactory, ObjectMapper parser) { this.characterFactory = characterFactory; this.parser = parser; } /** * Builds an object that can turn a JSON of the format described in class docs into a {@link GameCharacter}. * * @param characterFactory The factory to use to build the GameCharacter */ public CharacterLoader(CharacterFactory characterFactory) { this(characterFactory, JSON_PARSER); JSON_PARSER.configure(JsonParser.Feature.ALLOW_COMMENTS, true); } @Override public void load(Collection<String> data, GameState state) { data.stream().forEach(datum -> loadCharacter(datum, state)); } /** * Loads the character data in the specified file into the specified state. * * @param characterDataString The file containing the character data in JSON format. * @param state The state to load the character into. * * @throws IllegalStateException if an IOException is encountered while loading the character data. */ private void loadCharacter(String characterDataString, GameState state) { LOG.info(String.format("Loading character: %s", characterDataString)); try { JsonNode characterData = parser.readValue(characterDataString, JsonNode.class); verifyData(characterData); GameCharacter character = characterFactory.build(characterData.get(NAME).asText().toLowerCase(), hydratePrintedName(characterData.get("printedName")), characterData.get(BATTLE_NAME).asText(), characterData.get(DESCRIPTION).asText(), Gender.valueOf(characterData.get(GENDER).asText().toUpperCase()), hydrateAttackRanges(characterData.get(ATTACK_RANGES).elements()), hydrateStatistics(characterData.get(PRIMARY_STATISTICS), statisticName -> PrimaryStatisticName .valueOf(statisticName.replace(" ", "_").toUpperCase()).name(), Arrays.stream(PrimaryStatisticName.values())), Arrays.asList(SecondaryStatisticName.values()).stream().map(SecondaryStatisticName::name) .collect(Collectors.toList()), hydrateAppearance(characterData.get(APPEARANCE)), hydrateEquipSlots(characterData.get(EQUIPMENT), state), hydrateSkills(characterData.get(SKILLS), state), hydrateSpankingEvents(characterData.get("in combat spanking")), SpankingRole.valueOf(characterData.get(ROLE).asText().toUpperCase())); JsonNode secondaryStatistics = characterData.get(SECONDARY_STATISTICS); if (secondaryStatistics != null) { Map<String, Integer> hydratedStatistics = hydrateStatistics(secondaryStatistics, statisticName -> SecondaryStatisticName.valueOf(EnumUtils.nameToEnum(statisticName)).name(), Arrays.stream(SecondaryStatisticName.values())); hydratedStatistics.entrySet().stream().filter(entry -> entry.getValue() > 0) .forEach(entry -> character.setStatistic(entry.getKey(), entry.getValue())); character.setStatistic(SecondaryStatisticName.ENERGY.name(), character.getStatistic(SecondaryStatisticName.MAXIMUM_ENERGY.name())); } if (character.getName().equals(GameState.PC_NAME)) { state.setPlayerCharacter(character); } else { state.addNonPlayerCharacter(character); } } catch (IOException e) { LOG.log(Level.SEVERE, e.getMessage()); throw new IllegalStateException(e.getMessage()); } } /** * Verifies that all the data that should be in the JSON is there. * * @param characterData The data to be verified * * @throws IllegalArgumentException if some of the data is missing. */ private void verifyData(JsonNode characterData) { List<String> missingFields = new ArrayList<>(); if (characterData.get(NAME) == null) { missingFields.add(NAME); } if (characterData.get(PRINTED_NAME) == null) { missingFields.add(PRINTED_NAME); } if (characterData.get(DESCRIPTION) == null) { missingFields.add(DESCRIPTION); } if (characterData.get(GENDER) == null) { missingFields.add(GENDER); } if (characterData.get(ATTACK_RANGES) == null) { missingFields.add(ATTACK_RANGES); } if (characterData.get(PRIMARY_STATISTICS) == null) { missingFields.add(PRIMARY_STATISTICS); } if (characterData.get(EXPERIENCE) == null) { missingFields.add(EXPERIENCE); } if (characterData.get(APPEARANCE) == null) { missingFields.add(APPEARANCE); } if (characterData.get(EQUIPMENT) == null) { missingFields.add(EQUIPMENT); } if (!missingFields.isEmpty()) { String msg = String.format("Character %s is missing the fields:\n%s", characterData, String.join("\n", missingFields)); LOG.log(Level.SEVERE, msg); throw new IllegalArgumentException(msg); } } /** * Creates the map of printed names for this character. * * @param printedName The JsonNode containing the printed names * * @return The map of gendered printed names */ private Map<Gender, String> hydratePrintedName(JsonNode printedName) { String maleName = printedName.get(MALE).asText(); String femaleName = printedName.get(FEMALE).asText(); Map<Gender, String> printedNames = new HashMap<>(2); printedNames.put(Gender.MALE, maleName); printedNames.put(Gender.FEMALE, femaleName); return printedNames; } /** * Hydrates this character's spanking events * * @param spankingEvents The character's spanking events * * @return A map from strings describing the context of the spanking (i.e. position, whether it's the start * or continuation text, etc) to the EventDescriptions that model the dynamic text. Returns an empty map if * {@code spankingEvents} is null */ private Map<String, EventDescription> hydrateSpankingEvents(JsonNode spankingEvents) { if (spankingEvents == null) { return Collections.emptyMap(); } Map<String, EventDescription> hydratedSpankingEvents = new HashMap<>(); Iterator<String> spankingSituations = spankingEvents.fieldNames(); while (spankingSituations.hasNext()) { String spankingKey = spankingSituations.next(); if (parser == JSON_PARSER) { hydratedSpankingEvents.put(spankingKey, TextParser.parse(spankingEvents.get(spankingKey).asText())); } else { hydratedSpankingEvents.put(spankingKey, TextParser.parse(spankingEvents.get(spankingKey).asText(), "\n")); } } return hydratedSpankingEvents; } /** * Hydrates character equipment * * @param equipment The node describing the character's equip slots and equipment * * @return A mapping of named equip slots */ private LinkedHashMap<String, EquipSlot> hydrateEquipSlots(JsonNode equipment, GameState state) { Iterator<String> fieldNames = equipment.fieldNames(); LinkedHashMap<String, EquipSlot> equippedItems = new LinkedHashMap<>( CrimsonGlowEquipSlotNames.values().length); while (fieldNames.hasNext()) { String equipmentName = fieldNames.next(); String itemName = equipment.get(equipmentName).asText(); String equipSlotName = CrimsonGlowEquipSlotNames.valueOf(equipmentName.replace(' ', '_').toUpperCase()) .name(); equippedItems.put(equipSlotName, new EquipSlot(equipSlotName, (Equipment) state.getItem(itemName))); } return equippedItems; } /** * Hydrates the character's appearance. * * @param appearanceNode The JSON node describing this character's appearance * * @return A map of named appearance elements */ private Map<String, AppearanceElement> hydrateAppearance(JsonNode appearanceNode) { if (appearanceNode == null) { String msg = "Object 'appearance' does not appear in the JSON"; LOG.log(Level.SEVERE, msg); throw new IllegalArgumentException(msg); } Iterator<String> appearanceElementNames = appearanceNode.fieldNames(); Map<String, AppearanceElement> appearanceElements = new LinkedHashMap<>(); while (appearanceElementNames.hasNext()) { String appearanceElementName = appearanceElementNames.next(); String appearance = appearanceNode.get(appearanceElementName).asText(); AppearanceElement hydratedAppearanceElement = hydrateAppearanceElement( appearanceElementName.toLowerCase(), appearance); appearanceElements.put(appearanceElementName.toLowerCase(), hydratedAppearanceElement); } return appearanceElements; } /** * Given the name of the enumeration class, and a String representation of the appearance, returns the appropriate * AppearanceElement. * * @param appearanceElementName The name of the type of appearance element (i.e. "haircolor") * @param appearance The actual value for this appearance element (i.e. "brown") * * @return The hydrated appearance element * * @throws IllegalArgumentException if 'appearanceElementName' is not a valid appearance element */ private AppearanceElement hydrateAppearanceElement(String appearanceElementName, String appearance) { switch (appearanceElementName.toLowerCase()) { case EyeColor.EYECOLOR: return EyeColor.valueOf(appearance.toUpperCase()); case HairColor.HAIRCOLOR: return HairColor.valueOf(appearance.toUpperCase()); case HairStyle.HAIRSTYLE: return HairStyle.valueOf(appearance.toUpperCase().replace(" ", "_")); case SkinColor.SKINCOLOR: return SkinColor.valueOf(appearance.toUpperCase()); case Height.HEIGHT: return Height.valueOf(appearance.toUpperCase()); case BodyType.BODY_TYPE_NAME: return BodyType.valueOf(appearance.toUpperCase()); case Musculature.MUSCULATURE_NAME: return Musculature.valueOf(appearance.toUpperCase()); default: String msg = String.format( "%s is not a valid appearance element. Valid names are: eyecolor," + " haircolor, hairstyle, skincolor, bodytype, musculature, height.", appearanceElementName); LOG.log(Level.SEVERE, msg); throw new IllegalArgumentException(msg); } } /** * Hydrates the character's primary statistics. * * @param statistics The JSON nodes containing the character's statistics * * @return The hydrated statistics */ private LinkedHashMap<String, Integer> hydrateStatistics(JsonNode statistics, Function<String, String> hydrateStatName, Stream<Enum> allStatisticNames) { LinkedHashMap<String, Integer> hydratedStatistics = new LinkedHashMap<>( PrimaryStatisticName.values().length); Iterator<String> statisticNames = statistics.fieldNames(); while (statisticNames.hasNext()) { String jsonStatisticName = statisticNames.next(); String statisticName = hydrateStatName.apply(jsonStatisticName); hydratedStatistics.put(statisticName, statistics.get(jsonStatisticName).asInt()); } allStatisticNames.map(Enum::name).filter(statistic -> !hydratedStatistics.containsKey(statistic)) .forEach(statistic -> hydratedStatistics.put(statistic, 0)); return hydratedStatistics; } /** * Hydrates this character's skills. * * @param jsonNode The node containing a map from skill names to skill levels * * @return A map from skill to the character's level in that skill */ private Map<Skill, Integer> hydrateSkills(JsonNode jsonNode, GameState state) { Iterator<String> skillNames = jsonNode.fieldNames(); Map<Skill, Integer> skillMap = new LinkedHashMap<>(); while (skillNames.hasNext()) { String skill = skillNames.next(); if (state.getSkill(skill) == null) { String message = String.format("Skill %s does not exist, or has not been loaded", skill); LOG.severe(message); throw new IllegalArgumentException(message); } skillMap.put(state.getSkill(skill), jsonNode.get(skill).asInt()); } return skillMap; } /** * Hydrates this character's default attack ranges. * * @param attackRanges The ranges at which this character can use their regular attack * * @return The combat ranges this character can attack at */ private List<CombatRange> hydrateAttackRanges(Iterator<JsonNode> attackRanges) { List<CombatRange> ranges = new ArrayList<>(CombatRange.values().length); while (attackRanges.hasNext()) { ranges.add(CombatRange.valueOf(attackRanges.next().asText().toUpperCase())); } return ranges; } }