Dialogue markup and lines, shared common folder, item atlases, ids, seconds and key checks
This commit is contained in:
1 parent
3e9a0cbd34
commit
f900a0d01f
217 files changed
+6833
-6426
No files matched your search
+24
-19
@@ -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 ®ion;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int Anm2::animation_get(const std::string& name) const
|
||||
{
|
||||
auto found = animationIndices.find(name);
|
||||
|
||||
+12
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user