#include "character.h" #include "json.h" #include constexpr float FONT_SIZE = 20; constexpr const char* EFFECT_NAMES[] = {"animation", "big", "color", "delay", "oscillation", "scene", "screenshake", "shake", "silent", "small", "sound"}; constexpr const char* USE_NAMES[] = {"", "edible", "tool"}; struct SoundSource { const char* key; const char* section; DataFile file; }; constexpr SoundSource SOUND_SOURCES[] = { #define X(id, file, section, key) {key, section, file}, SOUND_IDS(X) #undef X }; static int utf8_length_get(const std::string& text) { int length = 0; for (unsigned char byte : text) length += (byte & 0xC0) != 0x80; return length; } Entry* Dialogue::entry_get(const std::string& label) { auto found = entries.find(label); return found == entries.end() ? nullptr : &found->second; } Entry* Dialogue::pool_get(const std::string& pool) { auto found = pools.find(pool); if (found == pools.end() || found->second.empty()) return nullptr; return entry_get(found->second[SDL_rand((Sint32)found->second.size())]); } Entry* Dialogue::hook_get(const std::string& hook) { auto found = hooks.find(hook); if (found == hooks.end()) return nullptr; Entry* entry = pool_get(found->second); return entry ? entry : entry_get(found->second); } void Character::preview_load(const std::string& path, Renderer& renderer) { Json json; json.load(path + "character.json"); if (!(isValid = json.isValid)) return; object root = json.root; folder = path; key = path.substr(0, path.size() - 1); key = key.substr(key.find_last_of('/') + 1); name = json_string(root, "name", key); description = json_string(root, "description"); credits = json_string(root, "credits"); color = json_color(root, "color", color); weight = json_float(root, "weight"); anm2Path = json_string(root, "anm2"); portrait = root["portrait"].error() ? nullptr : renderer.texture_get(folder + json_string(root, "portrait")); render = root["render"].error() ? nullptr : renderer.texture_get(folder + json_string(root, "render")); for (element entry : json_array(root, "stages")) { object fields; if (!entry.get(fields)) stages.push_back({json_string(fields, "animationSuffix"), json_string(fields, "dialoguePool"), json_float(fields, "weight"), json_int(fields, "area", -1)}); } } Anm2* Character::anm2_get(const std::string& path, Renderer& renderer, Audio& audio) { if (path.empty()) return nullptr; auto found = anm2s.find(path); if (found != anm2s.end()) return found->second.animations.empty() ? nullptr : &found->second; Anm2& loaded = anm2s[path]; loaded.load(folder + path, renderer, audio); return loaded.animations.empty() ? nullptr : &loaded; } // Sets an interact area's fields from source, keeping what it doesn't name (its type's settings). static void interact_parse(object source, InteractArea& area, Character& character, Audio& audio) { area.animation = json_string(source, "animation", area.animation); area.cursorActive = json_string(source, "cursorActive", area.cursorActive); area.cursorHover = json_string(source, "cursorHover", area.cursorHover); area.dialoguePool = json_string(source, "dialoguePool", area.dialoguePool); area.dialoguePoolFull = json_string(source, "dialoguePoolFull", area.dialoguePoolFull); area.glyph = json_string(source, "glyph", area.glyph); area.idleOnEnd = json_string(source, "idleOnEnd", area.idleOnEnd); area.digestionClick = json_float(source, "digestionClick", area.digestionClick); area.digestionHover = json_float(source, "digestionHover", area.digestionHover); area.scaleAmplitude = json_float(source, "scaleAmplitude", area.scaleAmplitude); area.scaleCycles = json_float(source, "scaleCycles", area.scaleCycles); area.scaleTime = json_float(source, "scaleTime", area.scaleTime); area.dialogueCooldown = json_int(source, "dialogueCooldown", area.dialogueCooldown); area.isHold = json_is(source, "isHold", area.isHold); if (!source["layer"].error()) area.layer = character.anm2 ? character.anm2->layer_get(json_string(source, "layer")) : -1; if (!source["null"].error()) area.null = character.anm2 ? character.anm2->null_get(json_string(source, "null")) : -1; if (!source["particles"].error()) area.particles = json_strings(source, "particles"); if (!source["sounds"].error()) area.sounds = audio.sounds_get(character.folder, json_strings(source, "sounds")); } static void character_parse(object root, Character& character, Renderer& renderer, Audio& audio) { character.anm2 = character.anm2_get(character.anm2Path, renderer, audio); character.weightMax = json_float(root, "weightMax", character.weight); character.capacity = json_float(root, "capacity"); character.capacityMin = json_float(root, "capacityMin"); character.capacityMax = json_float(root, "capacityMax", character.capacity); character.overstuffedMultiplier = json_float(root, "overstuffedMultiplier", 1); character.overstuffedDigestBonus = json_float(root, "overstuffedDigestBonus"); character.caloriesToKilogram = json_float(root, "caloriesToKilogram"); character.digestionRate = json_float(root, "digestionRate") / TICK_RATE; character.digestionRateMin = json_float(root, "digestionRateMin") / TICK_RATE; character.digestionRateMax = json_float(root, "digestionRateMax", character.digestionRate * TICK_RATE) / TICK_RATE; character.digestionTime = json_int(root, "digestionTime"); character.blinkChance = json_float(root, "blinkChance"); character.gurgleChance = json_float(root, "gurgleChance"); character.gurgleCapacityMultiplier = json_float(root, "gurgleCapacityMultiplier"); character.dialoguePool = json_string(root, "dialoguePool"); object alternate = json_object(root, "alternate"); if (!alternate["texture"].error()) character.alternateTexture = renderer.texture_get(character.folder + json_string(alternate, "texture")); if (!alternate["sound"].error()) character.alternateSound = audio.sound_get(character.folder + json_string(alternate, "sound")); character.alternateChance = json_float(alternate, "chance"); character.alternateSheet = json_int(alternate, "spritesheet"); object animations = json_object(root, "animations"); character.animationStart = json_string(animations, "start"); character.animationIdle = json_string(animations, "idle"); character.animationIdleFull = json_string(animations, "idleFull"); character.animationStageUp = json_string(animations, "stageUp"); Anm2* anm2 = character.anm2; auto layer_get = [&](object source, const char* key) { return anm2 ? anm2->layer_get(json_string(source, key)) : -1; }; auto null_get = [&](object source, const char* key) { return anm2 ? anm2->null_get(json_string(source, key)) : -1; }; character.talkSource = layer_get(json_object(root, "talk"), "source"); character.talkDestination = layer_get(json_object(root, "talk"), "destination"); character.blinkSource = layer_get(json_object(root, "blink"), "source"); character.blinkDestination = layer_get(json_object(root, "blink"), "destination"); for (element entry : json_array(root, "eatAreas")) { object fields; if (!entry.get(fields)) character.eatAreas.push_back({json_string(fields, "animation"), anm2 ? anm2->event_get(json_string(fields, "event")) : -1, null_get(fields, "null")}); } for (element entry : json_array(root, "expandAreas")) { object fields; if (!entry.get(fields)) character.expandAreas.push_back( {json_float(fields, "scaleMax"), layer_get(fields, "layer"), null_get(fields, "null")}); } std::vector typeNames; for (auto [name, value] : json_object(root, "interactTypes")) { object fields; if (value.get(fields)) continue; InteractArea& type = character.interactTypes.emplace_back(); interact_parse(fields, type, character, audio); type.name = name; type.type = (int)typeNames.size(); typeNames.emplace_back(name); } for (element entry : json_array(root, "interactAreas")) { object fields; if (entry.get(fields)) continue; auto type = std::find(typeNames.begin(), typeNames.end(), json_string(fields, "type")); InteractArea area = type == typeNames.end() ? InteractArea{} : character.interactTypes[type - typeNames.begin()]; interact_parse(fields, area, character, audio); if (area.null >= 0) character.interactAreas.push_back(area); } object areas = json_object(root, "areas"); Anm2* areaAnm2 = character.anm2_get(json_string(areas, "anm2"), renderer, audio); std::string boundsNull = json_string(areas, "boundsNull", "Bounds"); std::string spawnNull = json_string(areas, "spawnNull", boundsNull); for (element entry : json_array(areas, "list")) { object fields; if (entry.get(fields)) continue; Area& area = character.areas.emplace_back(); area.anm2 = fields["anm2"].error() ? areaAnm2 : character.anm2_get(json_string(fields, "anm2"), renderer, audio); area.gravity = json_float(fields, "gravity", area.gravity); area.friction = json_float(fields, "friction", area.friction); area.airResistance = json_float(fields, "airResistance", area.airResistance); if (!area.anm2) continue; Actor actor(area.anm2); area.bounds = actor.null_rect_get(area.anm2->null_get(boundsNull), -1, 0); area.spawn = actor.null_rect_get(area.anm2->null_get(spawnNull), -1, 0); area.spawn = is_rect_valid(area.spawn) ? area.spawn : area.bounds; } object cursor = json_object(root, "cursor"); character.cursor = character.anm2_get(json_string(cursor, "anm2"), renderer, audio); object cursorAnimations = json_object(cursor, "animations"); character.cursorIdle = json_string(cursorAnimations, "idle"); character.cursorClick = json_string(cursorAnimations, "click"); character.cursorHover = json_string(cursorAnimations, "hover"); character.cursorGrab = json_string(cursorAnimations, "grab"); character.cursorPan = json_string(cursorAnimations, "pan"); character.cursorZoom = json_string(cursorAnimations, "zoom"); character.cursorDispose = json_string(cursorAnimations, "dispose"); character.grabParticles = json_strings(json_object(cursor, "particles"), "grab"); object menu = json_object(root, "menu"); ImFontAtlas* fonts = ImGui::GetIO().Fonts; if (!menu["font"].error()) character.font = fonts->AddFontFromFileTTF((character.folder + json_string(menu, "font")).c_str(), FONT_SIZE); if (!menu["iconsFont"].error()) character.iconsFont = fonts->AddFontFromFileTTF((character.folder + json_string(menu, "iconsFont")).c_str(), FONT_SIZE); character.rounding = json_float(menu, "rounding"); character.homeGlyph = json_string(menu, "homeGlyph"); for (auto [name, value] : json_object(root, "particles")) { object fields; if (!value.get(fields)) character.particles[std::string(name)] = { json_string(fields, "animation"), character.anm2_get(json_string(fields, "anm2"), renderer, audio)}; } object strings = json_object(root, "strings"); for (int i = 0; i < STRING_COUNT; i++) character.strings[i] = json_string(strings, STRING_NAMES[i], STRING_DEFAULTS[i]); } static void items_parse(object root, Character& character, Renderer& renderer, Audio& audio) { Anm2* baseAnm2 = character.anm2_get(json_string(root, "anm2"), renderer, audio); character.itemSheet = json_int(root, "spritesheet"); character.spawnDelay = json_int(root, "spawnDelay", character.spawnDelay); character.spawnDelayMultiplier = json_float(root, "spawnDelayMultiplier", character.spawnDelayMultiplier); character.spawnLimit = json_int(root, "spawnLimit", character.spawnLimit); object particles = json_object(root, "particles"); character.disposeParticles = json_strings(particles, "dispose"); character.summonParticles = json_strings(particles, "summon"); object categories = json_object(root, "categories"); std::vector rarityNames; for (auto [name, value] : json_object(root, "rarities")) { object fields; if (value.get(fields)) continue; std::string sound = json_string(fields, "sound"); character.rarities.push_back({std::string(name), sound.empty() ? nullptr : audio.sound_get(character.folder + sound), json_float(fields, "weight")}); rarityNames.emplace_back(name); } for (element entry : json_array(root, "items")) { object fields; if (entry.get(fields)) continue; Item& item = character.items.emplace_back(); item.name = json_string(fields, "name", "???"); item.description = json_string(fields, "description", "???"); item.category = json_string(fields, "category"); item.flavor = json_string(fields, "flavor"); std::string use = json_string(categories, item.category.c_str()); for (int i = 0; i < (int)std::size(USE_NAMES); i++) if (use == USE_NAMES[i]) item.use = (ItemUse)i; auto rarity = std::find(rarityNames.begin(), rarityNames.end(), json_string(fields, "rarity")); item.rarity = rarity == rarityNames.end() ? -1 : (int)(rarity - rarityNames.begin()); item.anm2 = fields["anm2"].error() ? baseAnm2 : character.anm2_get(json_string(fields, "anm2"), renderer, audio); if (!fields["texture"].error()) item.texture = renderer.texture_get(character.folder + json_string(fields, "texture")); item.color = json_color(fields, "color", CLEAR); item.calories = json_float(fields, "calories"); item.capacityBonus = json_float(fields, "capacityBonus"); item.digestionBonus = json_float(fields, "digestionBonus") / TICK_RATE; item.gravity = json_float(fields, "gravity"); item.isToggleSpritesheet = json_is(fields, "isToggleSpritesheet"); } } static void dialogue_parse(object root, Character& character, Audio& audio) { Dialogue& dialogue = character.dialogue; dialogue.delay = std::max(1, json_int(root, "delay", dialogue.delay)); dialogue.blipDelay = json_int(root, "blipDelay"); dialogue.blip = audio.sounds_get(character.folder, json_strings(root, "blip")); for (auto [letter, value] : json_object(root, "pauses")) { object fields; if (!value.get(fields)) dialogue.pauses[std::string(letter)] = {json_int(fields, "delay"), json_is(fields, "isSilent")}; } for (auto [hook, value] : json_object(root, "hooks")) { std::string_view target; if (!value.get(target)) dialogue.hooks[std::string(hook)] = target; } for (auto [pool, value] : json_object(root, "pools")) { array list; if (value.get(list)) continue; std::vector& labels = dialogue.pools[std::string(pool)]; for (element label : list) { std::string_view text; if (!label.get(text)) labels.emplace_back(text); } } for (auto [label, value] : json_object(root, "entries")) { object fields; if (value.get(fields)) continue; Entry& entry = dialogue.entries[std::string(label)]; entry.text = json_string(fields, "text"); entry.animation = json_string(fields, "animation"); entry.next = json_string(fields, "next"); entry.length = utf8_length_get(entry.text); for (element choiceEntry : json_array(fields, "choices")) { object choice; if (!choiceEntry.get(choice)) entry.choices.push_back({json_string(choice, "next"), json_string(choice, "text", "")}); } for (element effectEntry : json_array(fields, "effects")) { object source; if (effectEntry.get(source)) continue; Effect& effect = entry.effects.emplace_back(); std::string type = json_string(source, "type"); for (int i = 0; i < EFFECT_COUNT; i++) if (type == EFFECT_NAMES[i]) effect.type = (EffectType)i; effect.start = json_int(source, "start"); effect.end = json_int(source, "end"); effect.delay = json_int(source, "delay"); effect.color = json_color(source, "color"); effect.animation = json_string(source, "animation"); if (!source["sound"].error()) effect.sound = audio.sound_get(character.folder + json_string(source, "sound")); entry.isScene |= effect.type == EFFECT_SCENE; } entry.isTerminal = entry.next.empty() && entry.choices.empty(); } } static void sounds_parse(object root, DataFile file, Character& character, Audio& audio) { for (int i = 0; i < SOUND_COUNT; i++) { const SoundSource& source = SOUND_SOURCES[i]; object section = *source.section ? json_object(root, source.section) : root; if (source.file == file) character.sounds[i] = audio.sounds_get(character.folder, json_strings(json_object(section, "sounds"), source.key)); } } void Character::load(Renderer& renderer, Audio& audio) { if (isLoaded) return; isLoaded = true; Json json; json.load(folder + "character.json"); if (json.isValid) { character_parse(json.root, *this, renderer, audio); sounds_parse(json.root, FILE_CHARACTER, *this, audio); } json.load(folder + "items.json"); if (json.isValid) { items_parse(json.root, *this, renderer, audio); sounds_parse(json.root, FILE_ITEMS, *this, audio); } json.load(folder + "dialogue.json"); if (json.isValid) dialogue_parse(json.root, *this, audio); } // An actor for an item, showing its texture in place of its anm2's item spritesheet. Actor Character::item_actor_get(int item) { Actor actor(items[item].anm2); actor.isStatic = true; if (items[item].texture && itemSheet >= 0 && itemSheet < (int)actor.sheets.size()) actor.sheets[itemSheet] = items[item].texture; return actor; } // The area a stage plays in: the last one a stage up to it names, or the first. int Character::area_get(int stage) { if (areas.empty()) return -1; int area = 0; for (int i = 0; i < stage && i < (int)stages.size(); i++) if (stages[i].area >= 0 && stages[i].area < (int)areas.size()) area = stages[i].area; return area; } std::string Character::save_path_get() { return pref_path_get("saves/" + key + ".txt"); } // Every folder in resources/characters holding a character.json, in name order, with its preview loaded. std::vector characters_load(Renderer& renderer) { std::string folder = path_get("characters/"); int count = 0; char** names = SDL_GlobDirectory(folder.c_str(), "*", 0, &count); std::vector sorted(names, names + (names ? count : 0)); SDL_free(names); std::sort(sorted.begin(), sorted.end()); std::vector characters; characters.reserve(sorted.size()); for (const std::string& name : sorted) { characters.emplace_back().preview_load(folder + name + "/", renderer); if (!characters.back().isValid) characters.pop_back(); } return characters; }