Dialogue markup and lines, shared common folder, item atlases, ids, seconds and key checks

This commit is contained in:
shweet committed 2026-11-12 22:51:11 -05:00
1 parent 3e9a0cbd34
commit f900a0d01f
217 files changed
+6833 -6426

No files matched your search

+24 -19
View File
@@ -7,14 +7,7 @@ constexpr Color LAYER_COLOR = {1, 0, 0, 1};
constexpr Color NULL_COLOR = {0, 0, 1, 1};
constexpr const char* EASING_NAMES[] = {"", "linear", "easeIn", "easeOut", "easeInOut"};
struct Region
{
Vec2 crop;
Vec2 pivot;
Vec2 size;
};
static Anm2Frame frame_parse(object source, const std::vector<Region>* regions)
static Anm2Frame frame_parse(object source, const std::vector<Anm2Region>* regions)
{
Anm2Frame frame;
int region = json_int(source, "region", -1);
@@ -41,7 +34,7 @@ static Anm2Frame frame_parse(object source, const std::vector<Region>* regions)
return frame;
}
static Anm2Track track_parse(object source, const char* key, const std::vector<Region>* regions = nullptr)
static Anm2Track track_parse(object source, const char* key, const std::vector<Anm2Region>* regions = nullptr)
{
Anm2Track track;
for (element entry : json_array(source, key))
@@ -55,7 +48,7 @@ static Anm2Track track_parse(object source, const char* key, const std::vector<R
// Reads a list of tracks and groups (each holding more), keyed by idKey, into items under parent.
static void items_parse(array list, const char* idKey, int parent, Anm2Animation& animation,
std::vector<Anm2Item>& items, const std::vector<std::vector<Region>>& sheetRegions,
std::vector<Anm2Item>& items, const std::vector<std::vector<Anm2Region>>& sheetRegions,
const std::vector<int>& layerSheets)
{
for (element entry : list)
@@ -81,7 +74,7 @@ static void items_parse(array list, const char* idKey, int parent, Anm2Animation
// Reads animations by name, along with those inside groups of them (which hold an "animations" object).
static void animations_parse(object list, std::vector<Anm2Animation>& animations,
const std::vector<std::vector<Region>>& sheetRegions, const std::vector<int>& layerSheets)
const std::vector<std::vector<Anm2Region>>& sheetRegions, const std::vector<int>& layerSheets)
{
for (auto [key, value] : list)
{
@@ -121,24 +114,24 @@ static void animations_parse(object list, std::vector<Anm2Animation>& animations
}
}
void Anm2::load(const std::string& path, Renderer& renderer, Audio& audio)
// Loads a document by its path within roots. Its spritesheets and sounds are found relative to it, in the same roots.
void Anm2::load(const std::string& path, const Roots& roots, Renderer& renderer, Audio& audio)
{
Json json;
json.load(path);
json.load(file_find(roots, path));
if (!json.isValid)
return;
object root = json.root;
std::string folder = path.substr(0, path.find_last_of('/') + 1);
fps = json_float(json_object(root, "info"), "fps", fps);
std::vector<std::vector<Region>> sheetRegions;
for (element entry : json_array(root, "spritesheets"))
{
object fields;
if (entry.get(fields))
continue;
sheets.push_back(renderer.texture_get(folder + json_string(fields, "path")));
std::vector<Region>& regions = sheetRegions.emplace_back();
sheets.push_back(renderer.texture_get(file_find(roots, folder + json_string(fields, "path"))));
std::vector<Anm2Region>& sheet = regions.emplace_back();
for (element regionEntry : json_array(fields, "regions"))
{
object region;
@@ -147,7 +140,7 @@ void Anm2::load(const std::string& path, Renderer& renderer, Audio& audio)
Vec2 size = json_vec2(region, "size");
std::string origin = json_string(region, "origin");
Vec2 pivot = origin == "center" ? size / 2 : origin == "topLeft" ? Vec2{} : json_vec2(region, "pivot");
regions.push_back({json_vec2(region, "crop"), pivot, size});
sheet.push_back({json_string(region, "name"), json_vec2(region, "crop"), pivot, size});
}
}
@@ -171,9 +164,11 @@ void Anm2::load(const std::string& path, Renderer& renderer, Audio& audio)
names_parse("events", events, nullptr);
std::vector<std::string> soundPaths;
names_parse("sounds", soundPaths, nullptr);
sounds = audio.sounds_get(folder, soundPaths);
for (std::string& sound : soundPaths)
sound = folder + sound;
sounds = audio.sounds_get(roots, soundPaths);
animations_parse(json_object(root, "animations"), animations, sheetRegions, layerSheets);
animations_parse(json_object(root, "animations"), animations, regions, layerSheets);
for (int i = 0; i < (int)animations.size(); i++)
animationIndices[animations[i].name] = i;
bounds.assign(animations.size(), {NAN, NAN, NAN, NAN});
@@ -186,6 +181,16 @@ static int name_get(const std::vector<std::string>& names, const std::string& na
return found == names.end() ? -1 : (int)(found - names.begin());
}
const Anm2Region* Anm2::region_get(int sheet, const std::string& name) const
{
if (sheet < 0 || sheet >= (int)regions.size())
return nullptr;
for (const Anm2Region& region : regions[sheet])
if (region.name == name)
return &region;
return nullptr;
}
int Anm2::animation_get(const std::string& name) const
{
auto found = animationIndices.find(name);
+12 -1
View File
@@ -60,6 +60,15 @@ struct Anm2Trigger
int event = -1;
};
// A named part of a spritesheet: its rectangle and pivot.
struct Anm2Region
{
std::string name;
Vec2 crop;
Vec2 pivot;
Vec2 size;
};
// Layers draw in order, the first at the bottom.
struct Anm2Animation
{
@@ -83,6 +92,7 @@ public:
std::vector<std::string> events;
std::vector<std::string> layers;
std::vector<std::string> nulls;
std::vector<std::vector<Anm2Region>> regions;
std::vector<Rect> bounds;
std::vector<Texture*> sheets;
std::vector<int> layerSheets;
@@ -90,7 +100,8 @@ public:
float fps = 30;
int defaultAnimation = -1;
void load(const std::string& path, Renderer& renderer, Audio& audio);
void load(const std::string& path, const Roots& roots, Renderer& renderer, Audio& audio);
const Anm2Region* region_get(int sheet, const std::string& name) const;
int animation_get(const std::string& name) const;
int event_get(const std::string& name) const;
int layer_get(const std::string& name) const;
+26 -2
View File
@@ -27,11 +27,35 @@ Sound* Audio::sound_get(const std::string& path)
return &sound;
}
Sounds Audio::sounds_get(const std::string& folder, const std::vector<std::string>& paths)
// Sounds by path, where a path with a "*" stands for every match in the first root holding any, in name order.
Sounds Audio::sounds_get(const Roots& roots, const std::vector<std::string>& paths)
{
Sounds list;
for (const std::string& path : paths)
list.push_back(sound_get(folder + path));
{
if (path.find('*') == std::string::npos)
{
list.push_back(sound_get(file_find(roots, path)));
continue;
}
std::string normal = path_normalize(path);
std::string folder = normal.substr(0, normal.find_last_of('/') + 1);
std::string pattern = normal.substr(folder.size());
for (const std::string& root : roots)
{
int count = 0;
char** names = SDL_GlobDirectory((root + folder).c_str(), pattern.c_str(), 0, &count);
std::vector<std::string> sorted(names, names + (names ? count : 0));
SDL_free(names);
std::sort(sorted.begin(), sorted.end());
for (const std::string& name : sorted)
list.push_back(sound_get(root + folder + name));
if (!sorted.empty())
break;
}
if (list.empty())
SDL_Log("No sounds match: %s", normal.c_str());
}
return list;
}
+2 -1
View File
@@ -1,5 +1,6 @@
#pragma once
#include "common.h"
#include <SDL3/SDL_audio.h>
#include <string>
#include <unordered_map>
@@ -26,7 +27,7 @@ class Audio
public:
void init();
Sound* sound_get(const std::string& path);
Sounds sounds_get(const std::string& folder, const std::vector<std::string>& paths);
Sounds sounds_get(const Roots& roots, const std::vector<std::string>& paths);
void play(Sound* sound);
void play(const Sounds& sounds);
void gain_set(float gain);
+1 -1
View File
@@ -225,7 +225,7 @@ void Body::update(std::vector<Event>& events)
}
}
values_clamp();
if (is_chance(traits.gurgleChance * capacity_percent_get() * traits.gurgleCapacityMultiplier * PERCENT))
if (is_chance(traits.gurgleChance * capacity_percent_get() * traits.gurgleCapacityMultiplier))
audio->play(character->sounds[SOUND_GURGLE]);
if (int current = stage_get(stats.weight); current != stage)
stage_set(current);
+196 -89
View File
@@ -3,8 +3,9 @@
#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* COMMON_FOLDER = "common/";
constexpr const char* TAG_NAMES[] = {"animation", "big", "color", "delay", "screenshake",
"shake", "silent", "small", "sound", "wave"};
constexpr const char* USE_NAMES[] = {"", "edible", "tool"};
constexpr const char* HOOK_NAMES[] = {
#define X(id, name) name,
@@ -25,12 +26,10 @@ constexpr SoundSource SOUND_SOURCES[] = {
#undef X
};
static int utf8_length_get(const std::string& text)
// Durations in data files are in seconds, and kept in ticks.
static int ticks_get(float seconds)
{
int length = 0;
for (unsigned char byte : text)
length += (byte & 0xC0) != 0x80;
return length;
return (int)std::round(seconds * TICK_RATE);
}
Entry* Dialogue::entry_get(const std::string& label)
@@ -58,10 +57,10 @@ bool Dialogue::is_hook(Hook hook)
return pools.contains(hooks[hook]) || entries.contains(hooks[hook]);
}
int Catalog::item_get(const std::string& name)
int Catalog::item_get(const std::string& id)
{
for (int i = 0; i < (int)items.size(); i++)
if (items[i].name == name)
if (items[i].id == id)
return i;
return -1;
}
@@ -74,16 +73,18 @@ void Character::preview_load(const std::string& path, Renderer& renderer)
return;
object root = json.root;
folder = path;
roots = {folder, path_get(COMMON_FOLDER)};
key = path.substr(0, path.size() - 1);
key = key.substr(key.find_last_of('/') + 1);
id = json_string(root, "id", key);
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"));
portrait = root["portrait"].error() ? nullptr : renderer.texture_get(file_find(roots, json_string(root, "portrait")));
render = root["render"].error() ? nullptr : renderer.texture_get(file_find(roots, json_string(root, "render")));
for (element entry : json_array(root, "stages"))
{
object fields;
@@ -101,13 +102,18 @@ Anm2* Character::anm2_get(const std::string& path, Renderer& renderer, Audio& au
if (found != anm2s.end())
return found->second.animations.empty() ? nullptr : &found->second;
Anm2& loaded = anm2s[path];
loaded.load(folder + path, renderer, audio);
loaded.load(path, roots, 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)
{
json_keys_check(source,
{"type", "null", "layer", "glyph", "isHold", "animation", "idleOnEnd", "cursorHover", "cursorActive",
"dialoguePool", "dialoguePoolFull", "dialogueCooldown", "digestionClick", "digestionHover",
"scaleAmplitude", "scaleCycles", "scaleTime", "sounds", "particles"},
"an interact area");
area.animation = json_string(source, "animation", area.animation);
area.cursorActive = json_string(source, "cursorActive", area.cursorActive);
area.cursorHover = json_string(source, "cursorHover", area.cursorHover);
@@ -116,11 +122,11 @@ static void interact_parse(object source, InteractArea& area, Character& charact
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.digestionHover = json_float(source, "digestionHover", area.digestionHover * TICK_RATE) / TICK_RATE;
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.scaleTime = ticks_get(json_float(source, "scaleTime", area.scaleTime / TICK_RATE));
area.dialogueCooldown = ticks_get(json_float(source, "dialogueCooldown", area.dialogueCooldown / TICK_RATE));
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;
@@ -129,11 +135,19 @@ static void interact_parse(object source, InteractArea& area, Character& charact
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"));
area.sounds = audio.sounds_get(character.roots, json_strings(source, "sounds"));
}
static void character_parse(object root, Character& character, Renderer& renderer, Audio& audio)
{
json_keys_check(root,
{"id", "name", "description", "credits", "render", "portrait", "anm2", "color", "weight", "weightMax",
"capacity", "capacityMin", "capacityMax", "overstuffedMultiplier", "overstuffedDigestBonus",
"caloriesToKilogram", "digestionRate", "digestionRateMin", "digestionRateMax", "digestionTime",
"blinkChance", "gurgleChance", "gurgleCapacityMultiplier", "dialoguePool", "alternate", "stages",
"animations", "talk", "blink", "eatAreas", "expandAreas", "sounds", "interactTypes",
"interactAreas", "areas", "cursor", "menu", "particles"},
"character.json");
character.anm2 = character.anm2_get(character.anm2Path, renderer, audio);
Traits& traits = character.traits;
traits.weightMax = json_float(root, "weightMax", traits.weight);
@@ -146,21 +160,23 @@ static void character_parse(object root, Character& character, Renderer& rendere
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.digestionTime = ticks_get(json_float(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");
json_keys_check(alternate, {"texture", "sound", "chance", "spritesheet"}, "alternate");
if (!alternate["texture"].error())
character.alternate.texture = renderer.texture_get(character.folder + json_string(alternate, "texture"));
character.alternate.texture = renderer.texture_get(file_find(character.roots, json_string(alternate, "texture")));
if (!alternate["sound"].error())
character.alternate.sound = audio.sound_get(character.folder + json_string(alternate, "sound"));
character.alternate.sound = audio.sound_get(file_find(character.roots, json_string(alternate, "sound")));
character.alternate.chance = json_float(alternate, "chance");
character.alternate.sheet = json_int(alternate, "spritesheet");
object animations = json_object(root, "animations");
json_keys_check(animations, {"start", "idle", "idleFull", "stageUp"}, "animations");
character.animations = {json_string(animations, "idle"), json_string(animations, "idleFull"),
json_string(animations, "stageUp"), json_string(animations, "start")};
@@ -171,24 +187,37 @@ static void character_parse(object root, Character& character, Renderer& rendere
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")};
auto swap_get = [&](const char* key) {
object swap = json_object(root, key);
json_keys_check(swap, {"source", "destination"}, key);
return Swap{layer_get(swap, "destination"), layer_get(swap, "source")};
};
character.talk = swap_get("talk");
character.blink = swap_get("blink");
for (element entry : json_array(root, "stages"))
{
object fields;
if (!entry.get(fields))
json_keys_check(fields, {"weight", "dialoguePool", "animationSuffix", "area"}, "a stage");
}
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")});
if (entry.get(fields))
continue;
json_keys_check(fields, {"null", "animation", "event"}, "an eat area");
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")});
if (entry.get(fields))
continue;
json_keys_check(fields, {"layer", "null", "scaleMax"}, "an expand area");
character.expandAreas.push_back(
{json_float(fields, "scaleMax"), layer_get(fields, "layer"), null_get(fields, "null")});
}
std::vector<std::string> typeNames;
@@ -216,6 +245,7 @@ static void character_parse(object root, Character& character, Renderer& rendere
}
object areas = json_object(root, "areas");
json_keys_check(areas, {"anm2", "boundsNull", "spawnNull", "list"}, "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);
@@ -224,6 +254,7 @@ static void character_parse(object root, Character& character, Renderer& rendere
object fields;
if (entry.get(fields))
continue;
json_keys_check(fields, {"anm2", "gravity", "friction", "airResistance"}, "an area");
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);
@@ -239,6 +270,8 @@ static void character_parse(object root, Character& character, Renderer& rendere
object cursor = json_object(root, "cursor");
object cursorAnimations = json_object(cursor, "animations");
json_keys_check(cursor, {"anm2", "animations", "sounds", "particles"}, "cursor");
json_keys_check(cursorAnimations, {"idle", "click", "hover", "grab", "pan", "zoom", "dispose"}, "cursor animations");
auto cursor_get = [&](const char* key) {
return json_string(cursorAnimations, key);
};
@@ -253,10 +286,12 @@ static void character_parse(object root, Character& character, Renderer& rendere
character.anm2_get(json_string(cursor, "anm2"), renderer, audio)};
object menu = json_object(root, "menu");
json_keys_check(menu, {"font", "iconsFont", "rounding", "homeGlyph", "sounds"}, "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);
: ImGui::GetIO().Fonts->AddFontFromFileTTF(file_find(character.roots, path).c_str(),
FONT_SIZE);
};
character.menu = {json_string(menu, "homeGlyph"), font_get("font"), font_get("iconsFont"),
json_float(menu, "rounding")};
@@ -264,34 +299,42 @@ static void character_parse(object root, Character& character, Renderer& rendere
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)};
if (value.get(fields))
continue;
json_keys_check(fields, {"anm2", "animation"}, "a particle");
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)
{
json_keys_check(root,
{"anm2", "spritesheet", "spawnDelay", "spawnDelayMultiplier", "spawnLimit", "sounds", "particles",
"rarities", "items"},
"items.json");
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.spawnDelay = ticks_get(json_float(root, "spawnDelay", catalog.spawnDelay / TICK_RATE));
catalog.spawnDelayMultiplier = json_float(root, "spawnDelayMultiplier", catalog.spawnDelayMultiplier);
catalog.spawnLimit = json_int(root, "spawnLimit", catalog.spawnLimit);
object particles = json_object(root, "particles");
json_keys_check(particles, {"dispose", "summon"}, "item particles");
catalog.disposeParticles = json_strings(particles, "dispose");
catalog.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;
json_keys_check(fields, {"weight", "sound"}, "a rarity");
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")});
catalog.rarities.push_back({std::string(name),
sound.empty() ? nullptr : audio.sound_get(file_find(character.roots, sound)),
json_float(fields, "weight")});
rarityNames.emplace_back(name);
}
@@ -300,21 +343,29 @@ static void items_parse(object root, Character& character, Renderer& renderer, A
object fields;
if (entry.get(fields))
continue;
json_keys_check(fields,
{"id", "name", "description", "category", "rarity", "flavor", "use", "anm2", "region", "color",
"calories", "capacityBonus", "digestionBonus", "gravity", "isToggleSpritesheet"},
"an item");
Item& item = catalog.items.emplace_back();
item.name = json_string(fields, "name", "???");
item.id = json_string(fields, "id", item.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());
std::string use = json_string(fields, "use");
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);
std::string region = json_string(fields, "region", item.name);
item.region = item.anm2 ? item.anm2->region_get(catalog.sheet, region) : nullptr;
if (!item.region)
SDL_Log("Item \"%s\" has no region \"%s\"", item.name.c_str(), region.c_str());
if (!fields["color"].error())
item.color = json_color(fields, "color");
item.calories = json_float(fields, "calories");
item.capacityBonus = json_float(fields, "capacityBonus");
item.digestionBonus = json_float(fields, "digestionBonus") / TICK_RATE;
@@ -323,71 +374,126 @@ static void items_parse(object root, Character& character, Renderer& renderer, A
}
}
// Takes a line's markup out of its text, into effects over its characters. A tag is [name] (or [name=value]), closed
// by [/name] for the ones over a range. [[ is a plain [.
static void markup_parse(const std::string& source, Line& line, const std::string& label, Character& character,
Audio& audio)
{
std::vector<Effect> open;
int at = 0;
for (size_t i = 0; i < source.size();)
{
size_t close = source.find(']', i);
bool isEscape = source[i] == '[' && i + 1 < source.size() && source[i + 1] == '[';
if (source[i] != '[' || isEscape || close == std::string::npos)
{
at += ((unsigned char)source[i] & 0xC0) != 0x80;
line.text += source[i];
i += isEscape ? 2 : 1;
continue;
}
std::string tag = source.substr(i + 1, close - i - 1);
i = close + 1;
bool isClosing = !tag.empty() && tag[0] == '/';
std::string name = tag.substr(isClosing, tag.find('=') - isClosing);
std::string value = tag.find('=') == std::string::npos ? "" : tag.substr(tag.find('=') + 1);
auto found = std::find_if(std::begin(TAG_NAMES), std::end(TAG_NAMES),
[&](const char* tagName) { return name == tagName; });
if (found == std::end(TAG_NAMES))
{
SDL_Log("Unknown tag \"[%s]\" in dialogue entry \"%s\"", tag.c_str(), label.c_str());
continue;
}
Effect effect = {.type = (EffectType)(found - std::begin(TAG_NAMES)), .start = at};
bool isRange = effect.type == EFFECT_BIG || effect.type == EFFECT_COLOR || effect.type == EFFECT_SHAKE ||
effect.type == EFFECT_SILENT || effect.type == EFFECT_SMALL || effect.type == EFFECT_WAVE;
if (isClosing)
{
auto opened = std::find_if(open.rbegin(), open.rend(), [&](Effect& entry) { return entry.type == effect.type; });
if (opened == open.rend())
continue;
opened->end = at;
line.effects.push_back(*opened);
open.erase(std::next(opened).base());
continue;
}
if (effect.type == EFFECT_COLOR)
SDL_sscanf(value.c_str(), "%f %f %f", &effect.color.r, &effect.color.g, &effect.color.b);
effect.animation = effect.type == EFFECT_ANIMATION ? value : "";
effect.sound = effect.type == EFFECT_SOUND ? audio.sound_get(file_find(character.roots, value)) : nullptr;
effect.delay = effect.type == EFFECT_DELAY ? ticks_get((float)SDL_atof(value.c_str())) : 0;
(isRange ? open : line.effects).push_back(effect);
}
for (Effect& effect : open)
{
effect.end = at;
line.effects.push_back(effect);
}
line.length = at;
}
static void dialogue_parse(object root, Character& character, Audio& audio)
{
json_keys_check(root, {"speed", "blipEvery", "blip", "pauses", "hooks", "entries"}, "dialogue.json");
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"));
float speed = json_float(root, "speed", TICK_RATE / dialogue.delay);
dialogue.delay = std::max(1, (int)std::round(TICK_RATE / std::max(speed, 1.0f)));
dialogue.blipDelay = json_int(root, "blipEvery");
dialogue.blip = audio.sounds_get(character.roots, 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")};
if (value.get(fields))
continue;
json_keys_check(fields, {"delay", "isSilent"}, "a pause");
dialogue.pauses[std::string(letter)] = {ticks_get(json_float(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<std::string>& labels = dialogue.pools[std::string(pool)];
for (element label : list)
{
std::string_view text;
if (!label.get(text))
labels.emplace_back(text);
}
}
dialogue.hooks[i] = json_string(hooks, HOOK_NAMES[i], HOOK_NAMES[i]);
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");
std::string name(label);
json_keys_check(fields, {"isScene", "pools", "lines", "choices", "next"}, "dialogue entry \"" + name + "\"");
Entry& entry = dialogue.entries[name];
entry.next = json_string(fields, "next");
entry.length = utf8_length_get(entry.text);
entry.isScene = json_is(fields, "isScene");
for (const std::string& pool : json_strings(fields, "pools"))
dialogue.pools[pool].push_back(name);
for (element lineEntry : json_array(fields, "lines"))
{
object source;
if (lineEntry.get(source))
continue;
json_keys_check(source, {"text", "animation"}, "a line of \"" + name + "\"");
Line& line = entry.lines.emplace_back();
line.animation = json_string(source, "animation");
markup_parse(json_string(source, "text"), line, name, character, audio);
}
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))
if (choiceEntry.get(choice))
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;
json_keys_check(choice, {"text", "next"}, "a choice of \"" + name + "\"");
entry.choices.push_back({json_string(choice, "next"), json_string(choice, "text", "<choice>")});
}
if (entry.lines.empty())
entry.lines.emplace_back();
entry.isTerminal = entry.next.empty() && entry.choices.empty();
}
for (auto& [label, entry] : dialogue.entries)
{
std::vector<std::string> nexts = {entry.next};
for (Choice& choice : entry.choices)
nexts.push_back(choice.next);
for (const std::string& next : nexts)
if (!next.empty() && !dialogue.entries.contains(next))
SDL_Log("Dialogue entry \"%s\" leads to missing \"%s\"", label.c_str(), next.c_str());
}
}
static void sounds_parse(object root, DataFile file, Character& character, Audio& audio)
@@ -397,7 +503,7 @@ static void sounds_parse(object root, DataFile file, Character& character, Audio
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));
character.sounds[i] = audio.sounds_get(character.roots, json_strings(json_object(section, "sounds"), source.key));
}
}
@@ -424,14 +530,15 @@ void Character::load(Renderer& renderer, Audio& audio)
dialogue_parse(json.root, *this, audio);
}
// An actor for an item, showing its texture in place of its anm2's item spritesheet.
// An actor for an item, showing its region in place of whatever its anm2's layers show from the items' 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;
for (int layer = 0; entry.anm2 && entry.region && layer < (int)actor.layers.size(); layer++)
if (entry.anm2->layerSheets[layer] == catalog.sheet)
actor.layers[layer] = {entry.region->crop, entry.region->pivot, {}, entry.region->size, true};
return actor;
}
@@ -449,7 +556,7 @@ int Character::area_get(int stage)
std::string Character::save_path_get()
{
return pref_path_get("saves/" + key + ".txt");
return pref_path_get("saves/" + id + ".txt");
}
// Every folder in resources/characters holding a character.json, in name order, with its preview loaded.
+22 -10
View File
@@ -2,6 +2,7 @@
#include "anm2.h"
#include <array>
#include <optional>
#include <unordered_map>
struct ImFont;
@@ -142,15 +143,17 @@ struct Rarity
float weight = 0;
};
// An item: its look (a region of the items' spritesheet), what it does and how it's described. Saves keep it by id.
struct Item
{
std::string category;
std::string description;
std::string flavor;
std::string id;
std::string name;
std::optional<Color> color;
Anm2* anm2 = nullptr;
Texture* texture = nullptr;
Color color = CLEAR;
const Anm2Region* region = nullptr;
float calories = 0;
float capacityBonus = 0;
float digestionBonus = 0;
@@ -166,17 +169,16 @@ enum EffectType
EFFECT_BIG,
EFFECT_COLOR,
EFFECT_DELAY,
EFFECT_OSCILLATION,
EFFECT_SCENE,
EFFECT_SCREENSHAKE,
EFFECT_SHAKE,
EFFECT_SILENT,
EFFECT_SMALL,
EFFECT_SOUND,
EFFECT_WAVE,
EFFECT_COUNT
};
// Something a dialogue line does over its characters, from start up to (not including) end.
// Something a line does over its characters from start up to (not including) end, or once at start.
struct Effect
{
std::string animation;
@@ -194,14 +196,21 @@ struct Choice
std::string text;
};
struct Entry
// One line of dialogue: its text (markup taken out), the effects the markup gave it, and an animation to start with.
struct Line
{
std::string animation;
std::string next;
std::string text;
std::vector<Choice> choices;
std::vector<Effect> effects;
int length = 0;
};
// Lines played in order, then the choices (or the entry) they lead to.
struct Entry
{
std::string next;
std::vector<Choice> choices;
std::vector<Line> lines;
bool isScene = false;
bool isTerminal = false;
};
@@ -212,7 +221,8 @@ struct Pause
bool isSilent = false;
};
// Dialogue entries by label, pools of them picked from at random, and the pool (or else entry) each hook plays.
// Dialogue entries by label, pools of them (named by the entries) picked from at random, and the pool (or else entry)
// each hook plays, which is the one named after the hook unless the hooks say otherwise.
struct Dialogue
{
std::array<std::string, HOOK_COUNT> hooks;
@@ -305,7 +315,7 @@ struct Catalog
int spawnDelay = 60;
int spawnLimit = 100;
int item_get(const std::string& name);
int item_get(const std::string& id);
};
// Everything a character folder holds. The preview (what the select screen shows) loads at startup, the rest when
@@ -319,6 +329,7 @@ public:
std::string description;
std::string dialoguePool;
std::string folder;
std::string id;
std::string key;
std::string name;
std::unordered_map<std::string, Anm2> anm2s;
@@ -329,6 +340,7 @@ public:
std::vector<InteractArea> interactAreas;
std::vector<InteractArea> interactTypes;
std::vector<Stage> stages;
Roots roots;
Alternate alternate;
Animations animations;
Catalog catalog;
+35
View File
@@ -6,6 +6,7 @@
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
constexpr const char* GAME_ID = "shweets-sim";
constexpr const char* GAME_NAME = "Shweet's Sim";
@@ -198,3 +199,37 @@ inline std::string pref_path_get(const std::string& relative)
SDL_free(folder);
return path;
}
// A path with "." and ".." worked out, so it can be looked for under any folder.
inline std::string path_normalize(const std::string& path)
{
std::vector<std::string> parts;
size_t start = 0;
while (start <= path.size())
{
size_t end = std::min(path.find('/', start), path.size());
std::string part = path.substr(start, end - start);
if (part == ".." && !parts.empty() && parts.back() != "..")
parts.pop_back();
else if (!part.empty() && part != ".")
parts.push_back(part);
start = end + 1;
}
std::string result;
for (const std::string& part : parts)
result += (result.empty() ? "" : "/") + part;
return result;
}
// Folders a character's files are looked for in, in order: its own, then resources/common.
using Roots = std::vector<std::string>;
inline std::string file_find(const Roots& roots, const std::string& relative)
{
std::string normal = path_normalize(relative);
for (const std::string& root : roots)
if (SDL_GetPathInfo((root + normal).c_str(), nullptr))
return root + normal;
SDL_Log("File not found: %s", normal.c_str());
return roots.empty() ? normal : roots.front() + normal;
}
+65 -37
View File
@@ -5,9 +5,9 @@ constexpr float LIFE_MAX = 10;
constexpr float SCREENSHAKE_MAGNITUDE = 0.03f;
constexpr int SCREENSHAKE_TIME = 15;
static bool is_silent(const Entry& entry, int at)
static bool is_silent(const Line& line, int at)
{
for (const Effect& effect : entry.effects)
for (const Effect& effect : line.effects)
if (effect.type == EFFECT_SILENT && effect.start <= at && at < effect.end)
return true;
return false;
@@ -40,20 +40,28 @@ void Text::set(Play& play, Entry* next, TextMode mode, const std::string& onEnd)
idleOnEnd = onEnd;
isScene = next->isScene;
}
line_start(play, 0,
mode == TEXT_SEQUENCE ? INTERRUPT_ALWAYS
: mode == TEXT_INTERRUPTIBLE ? INTERRUPT_IF_ALLOWED
: INTERRUPT_NEVER);
}
// Starts one of the entry's lines: its animation, and the delays and animations its effects begin with.
void Text::line_start(Play& play, int index, Interrupt interrupt)
{
line = index;
Line& current = *line_get();
isFinished = false;
isStarted = false;
isBlipQueued = false;
isAnimationStarted = false;
animationTimePrevious = 0;
index = 0;
this->index = 0;
tick = 0;
life = 0;
Interrupt interrupt = mode == TEXT_SEQUENCE ? INTERRUPT_ALWAYS
: mode == TEXT_INTERRUPTIBLE ? INTERRUPT_IF_ALLOWED
: INTERRUPT_NEVER;
play.body.play(next->animation, TRANSITION_NORMAL, interrupt);
play.body.play(current.animation, TRANSITION_NORMAL, interrupt);
delay = delayBase = play.character->dialogue.delay;
for (Effect& effect : next->effects)
for (Effect& effect : current.effects)
{
if (effect.start != 0)
continue;
@@ -62,14 +70,24 @@ void Text::set(Play& play, Entry* next, TextMode mode, const std::string& onEnd)
if (effect.type == EFFECT_ANIMATION)
play.body.play(effect.animation, TRANSITION_FORCE);
}
if (!next->text.empty())
play.body.isTalking = !is_silent(*next, 0);
if (!current.text.empty())
play.body.isTalking = !is_silent(current, 0);
}
Line* Text::line_get()
{
return entry ? &entry->lines[line] : nullptr;
}
bool Text::is_last_line()
{
return entry && line + 1 >= (int)entry->lines.size();
}
// Applies the effects coming up at a character: delays and animations, screen shakes and sounds.
void Text::effects_apply(Play& play, int at)
{
for (Effect& effect : entry->effects)
for (Effect& effect : line_get()->effects)
{
if (effect.start != at)
continue;
@@ -99,7 +117,7 @@ void Text::clear(Play& play)
bool Text::is_interruptible()
{
return !entry || entry->isTerminal;
return !entry || (entry->isTerminal && is_last_line());
}
// The player's click or key: shows the rest of the line, or else moves to the next entry or closes the text.
@@ -107,19 +125,20 @@ void Text::advance(Play& play)
{
if (!entry)
return;
Line& current = *line_get();
if (!isFinished)
{
for (int skipped = index + 1; skipped <= entry->length; skipped++)
for (int skipped = index + 1; skipped <= current.length; skipped++)
effects_apply(play, skipped);
index = entry->length;
isFinished = true;
play.body.isTalking = false;
isScene &= !entry->isTerminal;
index = current.length;
finish(play);
}
else if (!entry->choices.empty() || !entry->next.empty())
else if (!is_last_line() || !entry->choices.empty() || !entry->next.empty())
{
play.audio->play(play.character->sounds[SOUND_SELECT]);
if (!entry->choices.empty())
if (!is_last_line())
line_start(play, line + 1, INTERRUPT_ALWAYS);
else if (!entry->choices.empty())
choose(play, 0);
else
set(play, play.character->dialogue.entry_get(entry->next), TEXT_SEQUENCE);
@@ -134,17 +153,27 @@ void Text::choose(Play& play, int choice)
set(play, play.character->dialogue.entry_get(entry->choices[choice].next), TEXT_SEQUENCE);
}
// Reveals the next character when its delay is up, with its effects, blips and talking. Entries without text wait out
// their animation, and finished last entries close after a while.
// Marks the line shown in full. A scene ends with the last line of its last entry.
void Text::finish(Play& play)
{
isFinished = true;
isBlipQueued = false;
play.body.isTalking = false;
isScene &= !(entry->isTerminal && is_last_line());
}
// Reveals the next character when its delay is up, with its effects, blips and talking. Lines without text wait out
// their animation, and the finished last line of a last entry closes after a while.
void Text::update(Play& play)
{
if (!entry)
return;
Dialogue& dialogue = play.character->dialogue;
Line& current = *line_get();
if (!isStarted)
{
isStarted = true;
for (Effect& effect : entry->effects)
for (Effect& effect : current.effects)
if (effect.start == 0 && effect.type == EFFECT_SCREENSHAKE)
play.camera.shake_add(SCREENSHAKE_MAGNITUDE, SCREENSHAKE_TIME);
else if (effect.start == 0 && effect.type == EFFECT_SOUND)
@@ -156,9 +185,9 @@ void Text::update(Play& play)
index++;
delay = delayBase;
effects_apply(play, index);
auto pause = dialogue.pauses.find(letter_get(entry->text, index - 1));
auto pause = dialogue.pauses.find(letter_get(current.text, index - 1));
Pause found = pause == dialogue.pauses.end() ? Pause{} : pause->second;
bool isSilentEffect = is_silent(*entry, index - 1);
bool isSilentEffect = is_silent(current, index - 1);
delay += found.delay;
play.body.isTalking = !found.isSilent && !isSilentEffect;
bool isBlipStep = dialogue.blipDelay > 0 && index % dialogue.blipDelay == 0;
@@ -171,29 +200,28 @@ void Text::update(Play& play)
play.audio->play(dialogue.blip);
isBlipQueued = false;
}
if (index >= entry->length)
{
isFinished = true;
isBlipQueued = false;
play.body.isTalking = false;
isScene &= !entry->isTerminal;
}
if (index >= current.length)
finish(play);
}
if (entry->text.empty())
if (current.text.empty())
{
Actor& actor = play.body.actor;
Anm2Animation* animation = actor.animation_get();
bool isReady = entry->animation.empty() || !animation || !actor.isPlaying ||
bool isReady = current.animation.empty() || !animation || !actor.isPlaying ||
(!isAnimationStarted && animation->length <= 1) ||
(animation->isLoop && isAnimationStarted && actor.time < animationTimePrevious);
isAnimationStarted = true;
animationTimePrevious = actor.time;
if (entry->next.empty())
clear(play);
else if (isReady)
if (!isReady)
return;
if (!is_last_line())
line_start(play, line + 1, INTERRUPT_ALWAYS);
else if (!entry->next.empty())
set(play, dialogue.entry_get(entry->next), TEXT_SEQUENCE);
else if (entry->choices.empty())
clear(play);
}
else if (isFinished && entry->isTerminal && (life += TICK) > LIFE_MAX)
else if (isFinished && entry->isTerminal && is_last_line() && (life += TICK) > LIFE_MAX)
clear(play);
}
+7 -2
View File
@@ -11,8 +11,8 @@ enum TextMode
TEXT_SEQUENCE
};
// Plays out a dialogue entry a character at a time with its effects, then offers its choices or the entry it leads to.
// A scene holds the game still until its last entry finishes. The UI shows it and passes on the player's input.
// Plays out a dialogue entry's lines a character at a time with their effects, then offers its choices or the entry it
// leads to. A scene holds the game still until its last entry finishes. The UI shows it and passes on the player's input.
class Text
{
public:
@@ -23,6 +23,7 @@ public:
int delay = 0;
int delayBase = 0;
int index = 0;
int line = 0;
int tick = 0;
bool isAnimationStarted = false;
bool isBlipQueued = false;
@@ -35,8 +36,12 @@ public:
void advance(Play& play);
void choose(Play& play, int choice);
void clear(Play& play);
Line* line_get();
bool is_interruptible();
bool is_last_line();
private:
void effects_apply(Play& play, int at);
void finish(Play& play);
void line_start(Play& play, int index, Interrupt interrupt);
};
+12
View File
@@ -94,9 +94,13 @@ inline Color json_color(object source, const char* key, Color fallback = WHITE,
return {value.r / divisor, value.g / divisor, value.b / divisor, value.a / divisor};
}
// A list of strings, or one string as a list of it.
inline std::vector<std::string> json_strings(object source, const char* key)
{
std::vector<std::string> values;
std::string_view single;
if (!source[key].get(single))
return {std::string(single)};
for (element entry : json_array(source, key))
{
std::string_view value;
@@ -105,3 +109,11 @@ inline std::vector<std::string> json_strings(object source, const char* key)
}
return values;
}
// Logs any key of an object its reader doesn't know (likely a typo), naming where it is.
inline void json_keys_check(object source, std::initializer_list<const char*> keys, const std::string& where)
{
for (auto [key, value] : source)
if (std::find_if(keys.begin(), keys.end(), [&](const char* known) { return key == known; }) == keys.end())
SDL_Log("Unknown key \"%s\" in %s", std::string(key).c_str(), where.c_str());
}
+2 -2
View File
@@ -105,10 +105,10 @@ void Play::save()
std::string text = fields_write(body.stats) + fields_write(*this);
for (int i = 0; i < (int)items.discovered.size(); i++)
if (items.discovered[i])
text += std::string(KEY_DISCOVERED) + "=" + character->catalog.items[i].name + "\n";
text += std::string(KEY_DISCOVERED) + "=" + character->catalog.items[i].id + "\n";
for (ItemEntity& entity : items.list)
text += std::string(KEY_ITEM) + "=" + field_write(entity.actor.position) + " " + field_write(entity.velocity) +
" " + field_write(entity.actor.rotation.value_or(0)) + " " + character->catalog.items[entity.id].name +
" " + field_write(entity.actor.rotation.value_or(0)) + " " + character->catalog.items[entity.id].id +
"\n";
file_write(character->save_path_get(), text);
toast_add("Saving...");
+20 -18
View File
@@ -220,13 +220,13 @@ static float hash_unit(Uint32 value)
return (value & 0xffffu) / 65535.0f;
}
// The scale, color and offset a dialogue entry's effects give its character at an index.
static float letter_style_get(const Entry& entry, int at, float time, ImU32& color, ImVec2& offset)
// The scale, color and offset a dialogue line's effects give its character at an index.
static float letter_style_get(const Line& line, int at, float time, ImU32& color, ImVec2& offset)
{
float scale = 1;
color = ImGui::GetColorU32(ImGuiCol_Text);
offset = {};
for (const Effect& effect : entry.effects)
for (const Effect& effect : line.effects)
{
if (at < effect.start || at >= effect.end)
continue;
@@ -235,7 +235,7 @@ static float letter_style_get(const Entry& entry, int at, float time, ImU32& col
color = ImGui::GetColorU32(imvec4_get({effect.color.r, effect.color.g, effect.color.b, 1}));
else if (effect.type == EFFECT_BIG || effect.type == EFFECT_SMALL)
scale = effect.type == EFFECT_BIG ? TEXT_BIG : TEXT_SMALL;
else if (effect.type == EFFECT_OSCILLATION)
else if (effect.type == EFFECT_WAVE)
offset.y +=
std::sin((time * OSCILLATION_FREQUENCY + at * OSCILLATION_SPREAD) * 2 * PI) * OSCILLATION_AMPLITUDE;
else if (effect.type == EFFECT_SHAKE)
@@ -247,8 +247,8 @@ static float letter_style_get(const Entry& entry, int at, float time, ImU32& col
return scale;
}
// Draws a dialogue entry's first count characters with their effects, wrapping whole words to the window.
static void letters_draw(const Entry& entry, int count, float time)
// Draws a dialogue line's first count characters with their effects, wrapping whole words to the window.
static void letters_draw(const Line& line, int count, float time)
{
ImFont* font = ImGui::GetFont();
float fontSize = ImGui::GetFontSize();
@@ -256,11 +256,11 @@ static void letters_draw(const Entry& entry, int count, float time)
float wrap = start.x + ImGui::GetContentRegionAvail().x;
ImVec2 cursor = start;
float lineHeight = fontSize;
const char* end = entry.text.data() + entry.text.size();
const char* end = line.text.data() + line.text.size();
auto width_get = [&](const char* from, const char* to, int at) {
ImU32 color;
ImVec2 offset;
return font->CalcTextSizeA(fontSize * letter_style_get(entry, at, time, color, offset), FLT_MAX, 0, from, to).x;
return font->CalcTextSizeA(fontSize * letter_style_get(line, at, time, color, offset), FLT_MAX, 0, from, to).x;
};
auto line_break = [&]() {
cursor = {start.x, cursor.y + lineHeight};
@@ -268,7 +268,7 @@ static void letters_draw(const Entry& entry, int count, float time)
};
int at = 0;
for (const char* it = entry.text.data(); it < end && at < count; at++)
for (const char* it = line.text.data(); it < end && at < count; at++)
{
unsigned int codepoint = 0;
const char* next = it + ImTextCharFromUtf8(&codepoint, it, end);
@@ -294,7 +294,7 @@ static void letters_draw(const Entry& entry, int count, float time)
}
ImU32 color;
ImVec2 offset;
float scale = letter_style_get(entry, at, time, color, offset);
float scale = letter_style_get(line, at, time, color, offset);
float width = width_get(it, next, at);
if (cursor.x + width > wrap)
{
@@ -421,7 +421,9 @@ void Ui::text_draw(Play& play, ImVec2 screen)
{
Text& text = play.text;
Entry* entry = text.entry;
if (!entry || entry->text.empty())
Line* line = text.line_get();
bool isChoosing = entry && text.is_last_line() && !entry->choices.empty();
if (!entry || (line->text.empty() && !isChoosing))
return;
ImGuiStyle& style = ImGui::GetStyle();
ImVec2 size = {screen.x * TEXT_WIDTH - style.WindowPadding.x * 2,
@@ -441,13 +443,13 @@ void Ui::text_draw(Play& play, ImVec2 screen)
ImGui::EndTabBar();
}
ImGui::PushFont(nullptr, FONT_NORMAL);
letters_draw(*entry, text.index, (float)ImGui::GetTime());
letters_draw(*line, text.index, (float)ImGui::GetTime());
ImVec2 position = ImGui::GetWindowPos();
ImVec2 contentMin = {position.x + style.WindowPadding.x, position.y + style.WindowPadding.y};
ImVec2 contentMax = {position.x + ImGui::GetWindowWidth() - style.WindowPadding.x,
position.y + ImGui::GetWindowHeight() - style.WindowPadding.y};
if (text.isFinished && !entry->choices.empty())
if (text.isFinished && isChoosing)
{
float buttonY = contentMax.y - ImGui::GetFrameHeight();
float separatorY = buttonY - style.ItemSpacing.y;
@@ -473,7 +475,7 @@ void Ui::text_draw(Play& play, ImVec2 screen)
}
else
{
if (text.isFinished && !entry->next.empty())
if (text.isFinished && (!text.is_last_line() || !entry->next.empty()))
{
float side = ImGui::GetTextLineHeightWithSpacing();
ImVec2 center = {contentMax.x - side / 2 +
@@ -757,9 +759,9 @@ void Ui::catalog_tab_draw(Play& play)
for (int i = 0; i < (int)items.size(); i++)
{
ImGui::PushID(i);
bool isColored = play.items.discovered[i] && items[i].color.a > 0;
bool isColored = play.items.discovered[i] && items[i].color;
if (isColored)
style_push(items[i].color);
style_push(*items[i].color);
bool isCurrent = i == selectedItem;
if (isCurrent)
for (ImGuiCol id : {ImGuiCol_Button, ImGuiCol_ButtonHovered, ImGuiCol_ButtonActive})
@@ -795,9 +797,9 @@ void Ui::catalog_tab_draw(Play& play)
if (infoHeight <= 0)
return;
bool isColored = isDiscovered && items[selectedItem].color.a > 0;
bool isColored = isDiscovered && items[selectedItem].color;
if (isColored)
style_push(items[selectedItem].color);
style_push(*items[selectedItem].color);
if (ImGui::BeginChild("##Info", {available.x, infoHeight}))
{
ImGui::Separator();