464 lines
20 KiB
C++
464 lines
20 KiB
C++
#include "character.h"
|
|
#include "json.h"
|
|
#include <imgui.h>
|
|
|
|
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<std::string> 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<std::string> 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<std::string>& 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", "<choice>")});
|
|
}
|
|
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<Character> characters_load(Renderer& renderer)
|
|
{
|
|
std::string folder = path_get("characters/");
|
|
int count = 0;
|
|
char** names = SDL_GlobDirectory(folder.c_str(), "*", 0, &count);
|
|
std::vector<std::string> sorted(names, names + (names ? count : 0));
|
|
SDL_free(names);
|
|
std::sort(sorted.begin(), sorted.end());
|
|
std::vector<Character> 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;
|
|
}
|