com.spankingrpgs.scarletmoon.loader.CharacterLoader.java Source code

Java tutorial

Introduction

Here is the source code for com.spankingrpgs.scarletmoon.loader.CharacterLoader.java

Source

/*
 * 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;
    }
}