#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"}; constexpr const char* HOOK_NAMES[] = { #define X(id, name) name, HOOKS(X) #undef X }; 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(Hook hook) { Entry* entry = pool_get(hooks[hook]); return entry ? entry : entry_get(hooks[hook]); } bool Dialogue::is_hook(Hook hook) { return pools.contains(hooks[hook]) || entries.contains(hooks[hook]); } int Catalog::item_get(const std::string& name) { for (int i = 0; i < (int)items.size(); i++) if (items[i].name == name) return i; return -1; } 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); traits.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); Traits& traits = character.traits; traits.weightMax = json_float(root, "weightMax", traits.weight); traits.capacity = json_float(root, "capacity"); traits.capacityMin = json_float(root, "capacityMin"); traits.capacityMax = json_float(root, "capacityMax", traits.capacity); traits.overstuffedMultiplier = json_float(root, "overstuffedMultiplier", 1); traits.overstuffedDigestBonus = json_float(root, "overstuffedDigestBonus"); traits.caloriesToKilogram = json_float(root, "caloriesToKilogram"); traits.digestionRate = json_float(root, "digestionRate") / TICK_RATE; traits.digestionRateMin = json_float(root, "digestionRateMin") / TICK_RATE; traits.digestionRateMax = json_float(root, "digestionRateMax", traits.digestionRate * TICK_RATE) / TICK_RATE; traits.digestionTime = json_int(root, "digestionTime"); traits.blinkChance = json_float(root, "blinkChance"); traits.gurgleChance = json_float(root, "gurgleChance"); traits.gurgleCapacityMultiplier = json_float(root, "gurgleCapacityMultiplier"); character.dialoguePool = json_string(root, "dialoguePool"); object alternate = json_object(root, "alternate"); if (!alternate["texture"].error()) character.alternate.texture = renderer.texture_get(character.folder + json_string(alternate, "texture")); if (!alternate["sound"].error()) character.alternate.sound = audio.sound_get(character.folder + json_string(alternate, "sound")); character.alternate.chance = json_float(alternate, "chance"); character.alternate.sheet = json_int(alternate, "spritesheet"); object animations = json_object(root, "animations"); character.animations = {json_string(animations, "idle"), json_string(animations, "idleFull"), json_string(animations, "stageUp"), json_string(animations, "start")}; 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.talk = {layer_get(json_object(root, "talk"), "destination"), layer_get(json_object(root, "talk"), "source")}; character.blink = {layer_get(json_object(root, "blink"), "destination"), layer_get(json_object(root, "blink"), "source")}; 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"); object cursorAnimations = json_object(cursor, "animations"); auto cursor_get = [&](const char* key) { return json_string(cursorAnimations, key); }; character.cursor = {cursor_get("click"), cursor_get("dispose"), cursor_get("grab"), cursor_get("hover"), cursor_get("idle"), cursor_get("pan"), cursor_get("zoom"), json_strings(json_object(cursor, "particles"), "grab"), character.anm2_get(json_string(cursor, "anm2"), renderer, audio)}; object menu = json_object(root, "menu"); auto font_get = [&](const char* key) { std::string path = json_string(menu, key); return path.empty() ? nullptr : ImGui::GetIO().Fonts->AddFontFromFileTTF((character.folder + path).c_str(), FONT_SIZE); }; character.menu = {json_string(menu, "homeGlyph"), font_get("font"), font_get("iconsFont"), json_float(menu, "rounding")}; 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)}; } } static void items_parse(object root, Character& character, Renderer& renderer, Audio& audio) { Catalog& catalog = character.catalog; Anm2* baseAnm2 = character.anm2_get(json_string(root, "anm2"), renderer, audio); catalog.sheet = json_int(root, "spritesheet"); catalog.spawnDelay = json_int(root, "spawnDelay", catalog.spawnDelay); catalog.spawnDelayMultiplier = json_float(root, "spawnDelayMultiplier", catalog.spawnDelayMultiplier); catalog.spawnLimit = json_int(root, "spawnLimit", catalog.spawnLimit); object particles = json_object(root, "particles"); catalog.disposeParticles = json_strings(particles, "dispose"); catalog.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"); catalog.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 = catalog.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")}; } object hooks = json_object(root, "hooks"); for (int i = 0; i < HOOK_COUNT; i++) dialogue.hooks[i] = json_string(hooks, HOOK_NAMES[i]); 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) { Item& entry = catalog.items[item]; Actor actor(entry.anm2); actor.isStatic = true; if (entry.texture && catalog.sheet >= 0 && catalog.sheet < (int)actor.sheets.size()) actor.sheets[catalog.sheet] = entry.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; }