Split play into body, items, interactions, autofeed and camera, with events, commands and literal strings
This commit is contained in:
1 parent
167f1a5ddd
commit
3e9a0cbd34
21 files changed
+2504
-2431
No files matched your search
+136
-98
@@ -174,6 +174,9 @@ void Anm2::load(const std::string& path, Renderer& renderer, Audio& audio)
|
||||
sounds = audio.sounds_get(folder, soundPaths);
|
||||
|
||||
animations_parse(json_object(root, "animations"), animations, sheetRegions, layerSheets);
|
||||
for (int i = 0; i < (int)animations.size(); i++)
|
||||
animationIndices[animations[i].name] = i;
|
||||
bounds.assign(animations.size(), {NAN, NAN, NAN, NAN});
|
||||
defaultAnimation = std::max(0, animation_get(json_string(root, "defaultAnimation")));
|
||||
}
|
||||
|
||||
@@ -185,10 +188,8 @@ static int name_get(const std::vector<std::string>& names, const std::string& na
|
||||
|
||||
int Anm2::animation_get(const std::string& name) const
|
||||
{
|
||||
for (int i = 0; i < (int)animations.size(); i++)
|
||||
if (animations[i].name == name)
|
||||
return i;
|
||||
return -1;
|
||||
auto found = animationIndices.find(name);
|
||||
return found == animationIndices.end() ? -1 : found->second;
|
||||
}
|
||||
|
||||
int Anm2::event_get(const std::string& name) const
|
||||
@@ -230,11 +231,86 @@ static float ease(float t, Easing easing)
|
||||
}
|
||||
}
|
||||
|
||||
// The track's keyframe at a time, blended toward the next one if it eases. Past the end the last keyframe holds. An
|
||||
// empty track gives an invisible frame.
|
||||
Anm2Frame track_sample(const Anm2Track& track, float time)
|
||||
{
|
||||
Anm2Frame frame;
|
||||
frame.isVisible = !track.empty();
|
||||
time = std::max(time, 0.0f);
|
||||
float start = 0;
|
||||
for (size_t i = 0; i < track.size(); i++)
|
||||
{
|
||||
frame = track[i];
|
||||
if (time >= start + frame.delay && i + 1 < track.size())
|
||||
{
|
||||
start += frame.delay;
|
||||
continue;
|
||||
}
|
||||
if (frame.easing != EASING_NONE && i + 1 < track.size() && frame.delay > 1)
|
||||
{
|
||||
const Anm2Frame& next = track[i + 1];
|
||||
float t = ease((time - start) / frame.delay, frame.easing);
|
||||
frame.position = vec2_lerp(frame.position, next.position, t);
|
||||
frame.scale = vec2_lerp(frame.scale, next.scale, t);
|
||||
frame.rotation = std::lerp(frame.rotation, next.rotation, t);
|
||||
frame.tint = color_lerp(frame.tint, next.tint, t);
|
||||
frame.offset = color_lerp(frame.offset, next.offset, t);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
static Affine root_affine_get(const Anm2Frame& frame, Vec2 position = {})
|
||||
{
|
||||
return affine_get(frame.position + position, frame.rotation, frame.scale / PERCENT);
|
||||
}
|
||||
|
||||
static Affine layer_affine_get(const Anm2Frame& frame)
|
||||
{
|
||||
return affine_get(frame.position, frame.rotation, frame.scale / PERCENT, frame.pivot);
|
||||
}
|
||||
|
||||
static Affine null_affine_get(const Anm2Frame& frame)
|
||||
{
|
||||
return affine_get(frame.position, frame.rotation, {1, 1}, frame.scale / 2);
|
||||
}
|
||||
|
||||
// The smallest rectangle holding every visible layer over a whole animation, around its origin. Worked out once.
|
||||
Rect Anm2::bounds_get(int index)
|
||||
{
|
||||
if (index < 0 || index >= (int)animations.size())
|
||||
return {NAN, NAN, NAN, NAN};
|
||||
if (is_rect_valid(bounds[index]))
|
||||
return bounds[index];
|
||||
Anm2Animation& animation = animations[index];
|
||||
Actor actor(this, {}, animation.name);
|
||||
for (int time = 0; time < animation.length; time++)
|
||||
{
|
||||
Affine root = root_affine_get(track_sample(animation.root, (float)time));
|
||||
for (Anm2Item& layer : animation.layers)
|
||||
{
|
||||
Affine model = root;
|
||||
Color tint;
|
||||
Color offset;
|
||||
Anm2Frame frame = track_sample(layer.frames, (float)time);
|
||||
if (layer.isVisible && frame.isVisible && frame.size.x > 0 &&
|
||||
actor.is_group_visible(animation, layer.group, (float)time, model, tint, offset))
|
||||
bounds[index] = rect_merge(bounds[index], affine_bounds(model * layer_affine_get(frame), frame.size.x,
|
||||
frame.size.y));
|
||||
}
|
||||
}
|
||||
return bounds[index];
|
||||
}
|
||||
|
||||
Actor::Actor(Anm2* anm2, Vec2 position, const std::string& name) : anm2(anm2), position(position)
|
||||
{
|
||||
if (!anm2)
|
||||
return;
|
||||
sheets = anm2->sheets;
|
||||
layers.resize(anm2->layers.size());
|
||||
nullScales.resize(anm2->nulls.size());
|
||||
int index = anm2->animation_get(name);
|
||||
request_apply({anm2->animations[index >= 0 ? index : anm2->defaultAnimation].name, TRANSITION_FORCE});
|
||||
}
|
||||
@@ -247,8 +323,7 @@ Anm2Animation* Actor::animation_get(int index)
|
||||
|
||||
bool Actor::is_playing(const std::string& name)
|
||||
{
|
||||
Anm2Animation* current = animation_get();
|
||||
return isPlaying && current && current->name == name;
|
||||
return isPlaying && anm2 && animation >= 0 && anm2->animation_get(name) == animation;
|
||||
}
|
||||
|
||||
bool Actor::is_finished()
|
||||
@@ -268,8 +343,7 @@ void Actor::request_apply(const Request& request)
|
||||
int index = anm2 ? anm2->animation_get(request.animation) : -1;
|
||||
if (index < 0)
|
||||
{
|
||||
static std::unordered_set<std::string> missing;
|
||||
if (anm2 && missing.insert(request.animation).second)
|
||||
if (anm2 && anm2->missing.insert(request.animation).second)
|
||||
SDL_Log("Animation \"%s\" does not exist", request.animation.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -289,23 +363,20 @@ void Actor::request_apply(const Request& request)
|
||||
triggersPlayed.assign(anm2->animations[index].triggers.size(), false);
|
||||
}
|
||||
|
||||
void Actor::pulse_add(int layer, float amplitude, float cycles, float duration)
|
||||
{
|
||||
if (layer >= 0 && layer < (int)layers.size() && duration > 0)
|
||||
pulses.push_back({amplitude, cycles, duration, duration, layer});
|
||||
}
|
||||
|
||||
void Actor::update(Audio& audio)
|
||||
{
|
||||
if (pending)
|
||||
request_apply(*pending);
|
||||
pending.reset();
|
||||
|
||||
for (int i = 0; i < (int)overrides.size(); i++)
|
||||
{
|
||||
Override& override = overrides[i];
|
||||
if (override.timeStart <= 0)
|
||||
continue;
|
||||
float percent = std::clamp(override.time / override.timeStart, 0.0f, 1.0f);
|
||||
float wave = std::cos((1 - percent) * 2 * PI * override.cycles) * percent;
|
||||
override.scale = {std::abs(override.amplitude.x) * wave, std::abs(override.amplitude.y) * wave};
|
||||
if (--override.time <= 0)
|
||||
overrides.erase(overrides.begin() + i--);
|
||||
}
|
||||
for (int i = 0; i < (int)pulses.size(); i++)
|
||||
if (--pulses[i].time <= 0)
|
||||
pulses.erase(pulses.begin() + i--);
|
||||
|
||||
Anm2Animation* current = animation_get();
|
||||
if (!isPlaying || (current && current->isLoop))
|
||||
@@ -339,62 +410,50 @@ void Actor::update(Audio& audio)
|
||||
triggersPlayed.assign(triggersPlayed.size(), false);
|
||||
}
|
||||
|
||||
// The track's keyframe at a time, blended toward the next one if it eases, then changed by any overrides. Past the end
|
||||
// the last keyframe holds. An empty track gives an invisible frame.
|
||||
Anm2Frame Actor::frame_get(const Anm2Track& track, float frameTime, OverrideTarget target, int id)
|
||||
// A layer's keyframe at a time, with its swapped region, added scale and pulses.
|
||||
Anm2Frame Actor::layer_frame_get(const Anm2Item& layer, float frameTime)
|
||||
{
|
||||
Anm2Frame frame;
|
||||
if (track.empty())
|
||||
{
|
||||
frame.isVisible = false;
|
||||
Anm2Frame frame = track_sample(layer.frames, frameTime);
|
||||
if (layer.id < 0 || layer.id >= (int)layers.size())
|
||||
return frame;
|
||||
}
|
||||
frameTime = std::max(frameTime, 0.0f);
|
||||
float start = 0;
|
||||
for (size_t i = 0; i < track.size(); i++)
|
||||
LayerChange& change = layers[layer.id];
|
||||
if (change.isRegion)
|
||||
{
|
||||
frame = track[i];
|
||||
if (frameTime >= start + frame.delay && i + 1 < track.size())
|
||||
{
|
||||
start += frame.delay;
|
||||
continue;
|
||||
}
|
||||
if (frame.easing != EASING_NONE && i + 1 < track.size() && frame.delay > 1)
|
||||
{
|
||||
const Anm2Frame& next = track[i + 1];
|
||||
float t = ease((frameTime - start) / frame.delay, frame.easing);
|
||||
frame.position = vec2_lerp(frame.position, next.position, t);
|
||||
frame.scale = vec2_lerp(frame.scale, next.scale, t);
|
||||
frame.rotation = std::lerp(frame.rotation, next.rotation, t);
|
||||
frame.tint = color_lerp(frame.tint, next.tint, t);
|
||||
frame.offset = color_lerp(frame.offset, next.offset, t);
|
||||
}
|
||||
break;
|
||||
frame.crop = change.crop;
|
||||
frame.size = change.size;
|
||||
frame.pivot = change.pivot;
|
||||
}
|
||||
for (Override& override : overrides)
|
||||
frame.scale = frame.scale + change.scale;
|
||||
for (Pulse& pulse : pulses)
|
||||
{
|
||||
if (override.target != target || override.id != id)
|
||||
if (pulse.layer != layer.id)
|
||||
continue;
|
||||
if (override.isRegion)
|
||||
{
|
||||
frame.crop = override.crop;
|
||||
frame.size = override.size;
|
||||
frame.pivot = override.pivot;
|
||||
}
|
||||
if (override.isRotation)
|
||||
frame.rotation = override.rotation;
|
||||
frame.scale = frame.scale + override.scale;
|
||||
float percent = pulse.time / pulse.duration;
|
||||
float wave = std::abs(pulse.amplitude) * std::cos((1 - percent) * 2 * PI * pulse.cycles) * percent;
|
||||
frame.scale = frame.scale + Vec2{wave, wave};
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
static Affine root_affine_get(const Anm2Frame& frame, Vec2 position = {})
|
||||
Anm2Frame Actor::null_frame_get(const Anm2Item& null, float frameTime)
|
||||
{
|
||||
return affine_get(frame.position + position, frame.rotation, frame.scale / PERCENT);
|
||||
Anm2Frame frame = track_sample(null.frames, frameTime);
|
||||
if (null.id >= 0 && null.id < (int)nullScales.size())
|
||||
frame.scale = frame.scale + nullScales[null.id];
|
||||
return frame;
|
||||
}
|
||||
|
||||
Anm2Frame Actor::root_frame_get(Anm2Animation& current, float frameTime)
|
||||
{
|
||||
Anm2Frame frame = track_sample(current.root, frameTime);
|
||||
if (rotation)
|
||||
frame.rotation = *rotation;
|
||||
return frame;
|
||||
}
|
||||
|
||||
// The transform, tint and color offset a group (and its parents) give its items, or false if any is hidden.
|
||||
bool Actor::is_group_visible(Anm2Animation& current, int group, float frameTime, Affine& model, Color& tint, Color& offset)
|
||||
bool Actor::is_group_visible(Anm2Animation& current, int group, float frameTime, Affine& model, Color& tint,
|
||||
Color& offset)
|
||||
{
|
||||
if (group < 0 || group >= (int)current.groups.size())
|
||||
return true;
|
||||
@@ -403,24 +462,19 @@ bool Actor::is_group_visible(Anm2Animation& current, int group, float frameTime,
|
||||
return false;
|
||||
if (entry.root.empty())
|
||||
return true;
|
||||
Anm2Frame frame = frame_get(entry.root, frameTime);
|
||||
Anm2Frame frame = track_sample(entry.root, frameTime);
|
||||
model = model * root_affine_get(frame);
|
||||
tint = tint * frame.tint;
|
||||
offset = offset + frame.offset;
|
||||
return frame.isVisible;
|
||||
}
|
||||
|
||||
static Affine null_affine_get(const Anm2Frame& frame)
|
||||
{
|
||||
return affine_get(frame.position, frame.rotation, {1, 1}, frame.scale / 2);
|
||||
}
|
||||
|
||||
void Actor::render(Renderer& renderer, Affine view)
|
||||
{
|
||||
Anm2Animation* current = animation_get();
|
||||
if (!isVisible || !current)
|
||||
return;
|
||||
Anm2Frame root = frame_get(current->root, time);
|
||||
Anm2Frame root = root_frame_get(*current, time);
|
||||
Affine rootModel = view * root_affine_get(root, position);
|
||||
for (Anm2Item& layer : current->layers)
|
||||
{
|
||||
@@ -429,25 +483,25 @@ void Actor::render(Renderer& renderer, Affine view)
|
||||
Color offset = root.offset;
|
||||
if (!layer.isVisible || !is_group_visible(*current, layer.group, time, model, tint, offset))
|
||||
continue;
|
||||
Anm2Frame frame = frame_get(layer.frames, time, TARGET_LAYER, layer.id);
|
||||
Anm2Frame frame = layer_frame_get(layer, time);
|
||||
int sheet = layer.id >= 0 && layer.id < (int)anm2->layerSheets.size() ? anm2->layerSheets[layer.id] : -1;
|
||||
Texture* texture = sheet >= 0 && sheet < (int)sheets.size() ? sheets[sheet] : nullptr;
|
||||
if (!frame.isVisible || !texture)
|
||||
continue;
|
||||
model = model * affine_get(frame.position, frame.rotation, frame.scale / PERCENT, frame.pivot);
|
||||
model = model * layer_affine_get(frame);
|
||||
renderer.quad_draw(*texture, {frame.crop.x, frame.crop.y, frame.size.x, frame.size.y}, model,
|
||||
frame.tint * tint, frame.offset + offset);
|
||||
if (isShowLayers)
|
||||
if (renderer.isShowLayers)
|
||||
renderer.outline_draw(model, frame.size.x, frame.size.y, LAYER_COLOR);
|
||||
}
|
||||
if (!isShowNulls)
|
||||
if (!renderer.isShowNulls)
|
||||
return;
|
||||
for (Anm2Item& null : current->nulls)
|
||||
{
|
||||
Affine model = rootModel;
|
||||
Color tint;
|
||||
Color offset;
|
||||
Anm2Frame frame = frame_get(null.frames, time, TARGET_NULL, null.id);
|
||||
Anm2Frame frame = null_frame_get(null, time);
|
||||
if (null.isVisible && frame.isVisible && is_group_visible(*current, null.group, time, model, tint, offset))
|
||||
renderer.outline_draw(model * null_affine_get(frame), frame.scale.x, frame.scale.y, NULL_COLOR);
|
||||
}
|
||||
@@ -464,10 +518,10 @@ Rect Actor::null_rect_get(int null, int index, float frameTime)
|
||||
{
|
||||
if (item.id != null)
|
||||
continue;
|
||||
Affine model = root_affine_get(frame_get(current->root, frameTime), position);
|
||||
Affine model = root_affine_get(root_frame_get(*current, frameTime), position);
|
||||
Color tint;
|
||||
Color offset;
|
||||
Anm2Frame frame = frame_get(item.frames, frameTime, TARGET_NULL, null);
|
||||
Anm2Frame frame = null_frame_get(item, frameTime);
|
||||
if (!frame.isVisible || !is_group_visible(*current, item.group, frameTime, model, tint, offset))
|
||||
break;
|
||||
return affine_bounds(model * null_affine_get(frame), frame.scale.x, frame.scale.y);
|
||||
@@ -475,28 +529,12 @@ Rect Actor::null_rect_get(int null, int index, float frameTime)
|
||||
return {NAN, NAN, NAN, NAN};
|
||||
}
|
||||
|
||||
// The smallest rectangle holding every visible layer over the whole current animation.
|
||||
// The current animation's bounds (see Anm2::bounds_get) where the actor stands, turned by its set rotation.
|
||||
Rect Actor::bounds_get()
|
||||
{
|
||||
Rect bounds = {NAN, NAN, NAN, NAN};
|
||||
Anm2Animation* current = animation_get();
|
||||
if (!current)
|
||||
return bounds;
|
||||
for (int frameTime = 0; frameTime < current->length; frameTime++)
|
||||
{
|
||||
Affine rootModel = root_affine_get(frame_get(current->root, (float)frameTime), position);
|
||||
for (Anm2Item& layer : current->layers)
|
||||
{
|
||||
Affine model = rootModel;
|
||||
Color tint;
|
||||
Color offset;
|
||||
Anm2Frame frame = frame_get(layer.frames, (float)frameTime, TARGET_LAYER, layer.id);
|
||||
if (!layer.isVisible || !frame.isVisible || frame.size.x <= 0 ||
|
||||
!is_group_visible(*current, layer.group, (float)frameTime, model, tint, offset))
|
||||
continue;
|
||||
model = model * affine_get(frame.position, frame.rotation, frame.scale / PERCENT, frame.pivot);
|
||||
bounds = rect_merge(bounds, affine_bounds(model, frame.size.x, frame.size.y));
|
||||
}
|
||||
}
|
||||
return bounds;
|
||||
Rect local = anm2 ? anm2->bounds_get(animation) : Rect{NAN, NAN, NAN, NAN};
|
||||
if (!is_rect_valid(local))
|
||||
return local;
|
||||
Affine model = affine_get(position, rotation.value_or(0), {1, 1}) * Affine{1, 0, 0, 1, local.x, local.y};
|
||||
return affine_bounds(model, local.w, local.h);
|
||||
}
|
||||
+31
-25
@@ -4,6 +4,8 @@
|
||||
#include "renderer.h"
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
enum Easing
|
||||
@@ -75,10 +77,13 @@ struct Anm2Animation
|
||||
class Anm2
|
||||
{
|
||||
public:
|
||||
std::unordered_map<std::string, int> animationIndices;
|
||||
std::unordered_set<std::string> missing;
|
||||
std::vector<Anm2Animation> animations;
|
||||
std::vector<std::string> events;
|
||||
std::vector<std::string> layers;
|
||||
std::vector<std::string> nulls;
|
||||
std::vector<Rect> bounds;
|
||||
std::vector<Texture*> sheets;
|
||||
std::vector<int> layerSheets;
|
||||
Sounds sounds;
|
||||
@@ -90,9 +95,11 @@ public:
|
||||
int event_get(const std::string& name) const;
|
||||
int layer_get(const std::string& name) const;
|
||||
int null_get(const std::string& name) const;
|
||||
Rect bounds_get(int animation);
|
||||
};
|
||||
|
||||
int track_length_get(const Anm2Track& track);
|
||||
Anm2Frame track_sample(const Anm2Track& track, float time);
|
||||
|
||||
enum Transition
|
||||
{
|
||||
@@ -108,30 +115,24 @@ enum Interrupt
|
||||
INTERRUPT_NEVER
|
||||
};
|
||||
|
||||
enum OverrideTarget
|
||||
// How an actor changes one of its layers: a region shown in place of its own, and scale added on.
|
||||
struct LayerChange
|
||||
{
|
||||
TARGET_ROOT,
|
||||
TARGET_LAYER,
|
||||
TARGET_NULL
|
||||
};
|
||||
|
||||
// Changes one track's frames as an actor draws them: a swapped region, an added scale (oscillating out over time, when
|
||||
// timed) or a set rotation.
|
||||
struct Override
|
||||
{
|
||||
Vec2 amplitude;
|
||||
Vec2 crop;
|
||||
Vec2 pivot;
|
||||
Vec2 scale;
|
||||
Vec2 size;
|
||||
float cycles = 0;
|
||||
float rotation = 0;
|
||||
float time = 0;
|
||||
float timeStart = 0;
|
||||
int id = -1;
|
||||
OverrideTarget target = TARGET_LAYER;
|
||||
bool isRegion = false;
|
||||
bool isRotation = false;
|
||||
};
|
||||
|
||||
// Scale added to a layer that oscillates and fades out over its duration, in ticks.
|
||||
struct Pulse
|
||||
{
|
||||
float amplitude = 0;
|
||||
float cycles = 1;
|
||||
float duration = 0;
|
||||
float time = 0;
|
||||
int layer = -1;
|
||||
};
|
||||
|
||||
struct Request
|
||||
@@ -141,17 +142,18 @@ struct Request
|
||||
Interrupt interrupt = INTERRUPT_IF_ALLOWED;
|
||||
};
|
||||
|
||||
// Plays an Anm2's animations by name at a position. A static actor holds its animation's first frame.
|
||||
// Plays an Anm2's animations by name at a position. A static actor holds its animation's first frame. Locked, it ignores
|
||||
// requests to play.
|
||||
class Actor
|
||||
{
|
||||
public:
|
||||
static inline bool isShowLayers = false;
|
||||
static inline bool isShowNulls = false;
|
||||
|
||||
Anm2* anm2 = nullptr;
|
||||
std::optional<Request> pending;
|
||||
std::vector<Override> overrides;
|
||||
std::optional<float> rotation;
|
||||
std::vector<LayerChange> layers;
|
||||
std::vector<Pulse> pulses;
|
||||
std::vector<Texture*> sheets;
|
||||
std::vector<Vec2> nullScales;
|
||||
std::vector<bool> triggersPlayed;
|
||||
Vec2 position;
|
||||
float time = 0;
|
||||
@@ -167,16 +169,20 @@ public:
|
||||
Actor(Anm2* anm2, Vec2 position = {}, const std::string& animation = "");
|
||||
void play(const std::string& name, Transition transition = TRANSITION_NORMAL,
|
||||
Interrupt interrupt = INTERRUPT_IF_ALLOWED);
|
||||
void pulse_add(int layer, float amplitude, float cycles, float duration);
|
||||
void update(Audio& audio);
|
||||
void render(Renderer& renderer, Affine view);
|
||||
Anm2Animation* animation_get(int index = -1);
|
||||
Anm2Frame frame_get(const Anm2Track& track, float frameTime, OverrideTarget target = TARGET_ROOT, int id = -1);
|
||||
Anm2Frame layer_frame_get(const Anm2Item& layer, float frameTime);
|
||||
Rect null_rect_get(int null, int index = -1, float frameTime = -1);
|
||||
Rect bounds_get();
|
||||
bool is_playing(const std::string& name);
|
||||
bool is_finished();
|
||||
bool is_group_visible(Anm2Animation& animation, int group, float frameTime, Affine& model, Color& tint,
|
||||
Color& offset);
|
||||
|
||||
private:
|
||||
void request_apply(const Request& request);
|
||||
bool is_group_visible(Anm2Animation& animation, int group, float frameTime, Affine& model, Color& tint, Color& offset);
|
||||
Anm2Frame null_frame_get(const Anm2Item& null, float frameTime);
|
||||
Anm2Frame root_frame_get(Anm2Animation& animation, float frameTime);
|
||||
};
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
#include "play.h"
|
||||
|
||||
constexpr float SWAP_STEP = 0.5f;
|
||||
|
||||
enum SwapMode
|
||||
{
|
||||
SWAP_LOOP,
|
||||
SWAP_ONCE
|
||||
};
|
||||
|
||||
// Keeps a value within a range given either way round, putting what isn't a number at its low end.
|
||||
static float value_clamp(float value, float low, float high)
|
||||
{
|
||||
if (low > high)
|
||||
std::swap(low, high);
|
||||
return std::isfinite(value) ? std::clamp(value, low, high) : low;
|
||||
}
|
||||
|
||||
// Starts a body from the character's traits, keeping any stats already loaded into it.
|
||||
void Body::set(Character& played, Audio& sound, Game game)
|
||||
{
|
||||
character = &played;
|
||||
audio = &sound;
|
||||
actor = Actor(played.anm2);
|
||||
Traits& traits = played.traits;
|
||||
if (stats.weight <= 0)
|
||||
{
|
||||
stats.weight = traits.weight;
|
||||
stats.capacity = traits.capacity;
|
||||
stats.digestionRate = traits.digestionRate;
|
||||
}
|
||||
bool isRolled = game == GAME_NEW && played.alternate.texture && is_chance(played.alternate.chance);
|
||||
stats.isAlternate |= isRolled;
|
||||
if (isRolled)
|
||||
audio->play(played.alternate.sound);
|
||||
if (stats.isAlternate && played.alternate.sheet < (int)actor.sheets.size())
|
||||
actor.sheets[played.alternate.sheet] = played.alternate.texture;
|
||||
values_clamp();
|
||||
stage_set(stage_get(stats.weight));
|
||||
idle_play();
|
||||
actor.update(*audio);
|
||||
}
|
||||
|
||||
void Body::stage_set(int index)
|
||||
{
|
||||
stage = index;
|
||||
suffix = stage > 0 && stage <= (int)character->stages.size() ? character->stages[stage - 1].animationSuffix : "";
|
||||
}
|
||||
|
||||
float Body::capacity_max_get()
|
||||
{
|
||||
return stats.capacity * character->traits.overstuffedMultiplier;
|
||||
}
|
||||
|
||||
float Body::capacity_percent_get()
|
||||
{
|
||||
float capacityMax = capacity_max_get();
|
||||
return capacityMax > 0 ? stats.calories / capacityMax : 0;
|
||||
}
|
||||
|
||||
bool Body::is_over_capacity()
|
||||
{
|
||||
return stats.calories > stats.capacity;
|
||||
}
|
||||
|
||||
int Body::stage_get(float weight)
|
||||
{
|
||||
std::vector<Stage>& stages = character->stages;
|
||||
if (stages.empty() || weight <= character->traits.weight)
|
||||
return 0;
|
||||
for (int i = 0; i < (int)stages.size(); i++)
|
||||
if (weight < stages[i].weight)
|
||||
return i;
|
||||
return (int)stages.size();
|
||||
}
|
||||
|
||||
float Body::stage_threshold_get(int index)
|
||||
{
|
||||
std::vector<Stage>& stages = character->stages;
|
||||
if (index <= 0 || stages.empty())
|
||||
return character->traits.weight;
|
||||
return stages[std::min(index, (int)stages.size()) - 1].weight;
|
||||
}
|
||||
|
||||
float Body::stage_progress_get()
|
||||
{
|
||||
float low = stage_threshold_get(stage);
|
||||
float high = stage_threshold_get(stage + 1);
|
||||
if (stage >= (int)character->stages.size() || high <= low)
|
||||
return 1;
|
||||
return (stats.weight - low) / (high - low);
|
||||
}
|
||||
|
||||
std::string Body::idle_get()
|
||||
{
|
||||
if (isStageUp)
|
||||
return "";
|
||||
if (!idleOverride.empty())
|
||||
return idleOverride;
|
||||
Animations& animations = character->animations;
|
||||
return is_over_capacity() && !animations.idleFull.empty() ? animations.idleFull : animations.idle;
|
||||
}
|
||||
|
||||
// Plays one of the character's animations, as its current stage draws it.
|
||||
void Body::play(const std::string& animation, Transition transition, Interrupt interrupt)
|
||||
{
|
||||
if (!animation.empty())
|
||||
actor.play(animation + suffix, transition, interrupt);
|
||||
}
|
||||
|
||||
void Body::idle_play()
|
||||
{
|
||||
play(idle_get());
|
||||
}
|
||||
|
||||
void Body::idle_override_set(const std::string& animation)
|
||||
{
|
||||
if (!animation.empty())
|
||||
idleOverride = animation == character->animations.idle ? "" : animation;
|
||||
}
|
||||
|
||||
// Where an eat area takes food: its null now, or at the start of its animation when it isn't showing.
|
||||
Rect Body::eat_rect_get(const EatArea& eatArea)
|
||||
{
|
||||
Rect rect = actor.null_rect_get(eatArea.null);
|
||||
if (!is_rect_valid(rect))
|
||||
rect = actor.null_rect_get(eatArea.null, actor.anm2->animation_get(eatArea.animation + suffix), 0);
|
||||
return rect;
|
||||
}
|
||||
|
||||
void Body::values_clamp()
|
||||
{
|
||||
Traits& traits = character->traits;
|
||||
stats.weight = value_clamp(stats.weight, traits.weight, traits.weightMax);
|
||||
stats.capacity = value_clamp(stats.capacity, traits.capacityMin, traits.capacityMax);
|
||||
stats.digestionRate = value_clamp(stats.digestionRate, traits.digestionRateMin, traits.digestionRateMax);
|
||||
stats.calories = value_clamp(stats.calories, 0, std::max(0.0f, capacity_max_get()));
|
||||
stats.digestionProgress = value_clamp(stats.digestionProgress, 0, DIGESTION_MAX);
|
||||
stats.digestionTimer = std::clamp(stats.digestionTimer, 0, std::max(0, traits.digestionTime));
|
||||
stats.totalCalories = std::max(0.0f, stats.totalCalories);
|
||||
stats.playtime = std::max(0.0f, stats.playtime);
|
||||
}
|
||||
|
||||
// Shows a swap's source layer on its destination, stepping through the source's frames while active, over and over or
|
||||
// once. Turns itself off (and returns false) when the animation lacks the source, or a single pass ends.
|
||||
static bool is_swapping(Actor& actor, Swap swap, float& time, bool isActive, SwapMode mode)
|
||||
{
|
||||
Anm2Animation* animation = actor.animation_get();
|
||||
bool isDestination = swap.destination >= 0 && swap.destination < (int)actor.layers.size();
|
||||
if (isDestination)
|
||||
actor.layers[swap.destination].isRegion = false;
|
||||
if (!isActive || !animation || !isDestination)
|
||||
return false;
|
||||
for (Anm2Item& layer : animation->layers)
|
||||
{
|
||||
if (layer.id != swap.source || layer.frames.empty())
|
||||
continue;
|
||||
Anm2Frame frame = actor.layer_frame_get(layer, time);
|
||||
LayerChange& change = actor.layers[swap.destination];
|
||||
change = {frame.crop, frame.pivot, change.scale, frame.size, true};
|
||||
time += SWAP_STEP;
|
||||
if (time < track_length_get(layer.frames))
|
||||
return true;
|
||||
time = 0;
|
||||
return mode == SWAP_LOOP;
|
||||
}
|
||||
time = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Digests, grows, gurgles, and keeps the body's animations up: idle, stage ups, swelling, talking and blinking.
|
||||
void Body::update(std::vector<Event>& events)
|
||||
{
|
||||
Traits& traits = character->traits;
|
||||
values_clamp();
|
||||
if (!actor.isPlaying)
|
||||
{
|
||||
if (isStageUp)
|
||||
{
|
||||
events.push_back(stage >= (int)character->stages.size() ? EVENT_STAGE_FINAL : EVENT_STAGE_UP);
|
||||
isStageUp = false;
|
||||
actor.isLocked = false;
|
||||
}
|
||||
if (!isTalking)
|
||||
idle_play();
|
||||
}
|
||||
actor.update(*audio);
|
||||
|
||||
if (stats.isDigesting && --stats.digestionTimer <= 0)
|
||||
{
|
||||
float nextWeight =
|
||||
value_clamp(stats.weight + stats.calories * traits.caloriesToKilogram, traits.weight, traits.weightMax);
|
||||
if (is_over_capacity())
|
||||
{
|
||||
float over = stats.calories - stats.capacity;
|
||||
float overPercent = over / (capacity_max_get() - stats.capacity);
|
||||
stats.capacity = value_clamp(stats.capacity + overPercent * traits.overstuffedDigestBonus * over,
|
||||
traits.capacityMin, traits.capacityMax);
|
||||
}
|
||||
stats.calories = 0;
|
||||
if (int nextStage = stage_get(nextWeight); nextStage > stage_get(stats.weight))
|
||||
{
|
||||
actor.interrupt = INTERRUPT_IF_ALLOWED;
|
||||
play(character->animations.stageUp, TRANSITION_NORMAL, INTERRUPT_NEVER);
|
||||
stage_set(nextStage);
|
||||
isStageUp = true;
|
||||
actor.isLocked = true;
|
||||
}
|
||||
else
|
||||
events.push_back(EVENT_DIGESTED);
|
||||
stats.weight = nextWeight;
|
||||
stats.isDigesting = false;
|
||||
stats.digestionTimer = traits.digestionTime;
|
||||
stats.digestionProgress = 0;
|
||||
}
|
||||
else if (!stats.isDigesting)
|
||||
{
|
||||
if (stats.calories > 0)
|
||||
stats.digestionProgress += stats.digestionRate;
|
||||
if (stats.digestionProgress >= DIGESTION_MAX)
|
||||
{
|
||||
stats.isDigesting = true;
|
||||
stats.digestionTimer = traits.digestionTime;
|
||||
audio->play(character->sounds[SOUND_DIGEST]);
|
||||
}
|
||||
}
|
||||
values_clamp();
|
||||
if (is_chance(traits.gurgleChance * capacity_percent_get() * traits.gurgleCapacityMultiplier * PERCENT))
|
||||
audio->play(character->sounds[SOUND_GURGLE]);
|
||||
if (int current = stage_get(stats.weight); current != stage)
|
||||
stage_set(current);
|
||||
|
||||
float stageProgress = stage_progress_get();
|
||||
float capacityProgress = capacity_percent_get();
|
||||
if (stats.isDigesting)
|
||||
capacityProgress *= (float)stats.digestionTimer / std::max(1, traits.digestionTime);
|
||||
for (ExpandArea& expandArea : character->expandAreas)
|
||||
{
|
||||
float add = std::clamp((stageProgress + capacityProgress) * expandArea.scaleMax / 2, 0.0f, expandArea.scaleMax);
|
||||
if (expandArea.layer >= 0)
|
||||
actor.layers[expandArea.layer].scale = {add, add};
|
||||
if (expandArea.null >= 0)
|
||||
actor.nullScales[expandArea.null] = {add, add};
|
||||
}
|
||||
|
||||
isTalking = is_swapping(actor, character->talk, talkTime, isTalking, SWAP_LOOP);
|
||||
isBlinking = is_swapping(actor, character->blink, blinkTime, isBlinking || is_chance(traits.blinkChance), SWAP_ONCE);
|
||||
}
|
||||
+81
-71
@@ -6,6 +6,11 @@ constexpr float FONT_SIZE = 20;
|
||||
constexpr const char* EFFECT_NAMES[] = {"animation", "big", "color", "delay", "oscillation", "scene",
|
||||
"screenshake", "shake", "silent", "small", "sound"};
|
||||
constexpr const char* USE_NAMES[] = {"", "edible", "tool"};
|
||||
constexpr const char* HOOK_NAMES[] = {
|
||||
#define X(id, name) name,
|
||||
HOOKS(X)
|
||||
#undef X
|
||||
};
|
||||
|
||||
struct SoundSource
|
||||
{
|
||||
@@ -42,13 +47,23 @@ Entry* Dialogue::pool_get(const std::string& pool)
|
||||
return entry_get(found->second[SDL_rand((Sint32)found->second.size())]);
|
||||
}
|
||||
|
||||
Entry* Dialogue::hook_get(const std::string& hook)
|
||||
Entry* Dialogue::hook_get(Hook hook)
|
||||
{
|
||||
auto found = hooks.find(hook);
|
||||
if (found == hooks.end())
|
||||
return nullptr;
|
||||
Entry* entry = pool_get(found->second);
|
||||
return entry ? entry : entry_get(found->second);
|
||||
Entry* entry = pool_get(hooks[hook]);
|
||||
return entry ? entry : entry_get(hooks[hook]);
|
||||
}
|
||||
|
||||
bool Dialogue::is_hook(Hook hook)
|
||||
{
|
||||
return pools.contains(hooks[hook]) || entries.contains(hooks[hook]);
|
||||
}
|
||||
|
||||
int Catalog::item_get(const std::string& name)
|
||||
{
|
||||
for (int i = 0; i < (int)items.size(); i++)
|
||||
if (items[i].name == name)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
void Character::preview_load(const std::string& path, Renderer& renderer)
|
||||
@@ -65,7 +80,7 @@ void Character::preview_load(const std::string& path, Renderer& renderer)
|
||||
description = json_string(root, "description");
|
||||
credits = json_string(root, "credits");
|
||||
color = json_color(root, "color", color);
|
||||
weight = json_float(root, "weight");
|
||||
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"));
|
||||
@@ -120,35 +135,34 @@ static void interact_parse(object source, InteractArea& area, Character& charact
|
||||
static void character_parse(object root, Character& character, Renderer& renderer, Audio& audio)
|
||||
{
|
||||
character.anm2 = character.anm2_get(character.anm2Path, renderer, audio);
|
||||
character.weightMax = json_float(root, "weightMax", character.weight);
|
||||
character.capacity = json_float(root, "capacity");
|
||||
character.capacityMin = json_float(root, "capacityMin");
|
||||
character.capacityMax = json_float(root, "capacityMax", character.capacity);
|
||||
character.overstuffedMultiplier = json_float(root, "overstuffedMultiplier", 1);
|
||||
character.overstuffedDigestBonus = json_float(root, "overstuffedDigestBonus");
|
||||
character.caloriesToKilogram = json_float(root, "caloriesToKilogram");
|
||||
character.digestionRate = json_float(root, "digestionRate") / TICK_RATE;
|
||||
character.digestionRateMin = json_float(root, "digestionRateMin") / TICK_RATE;
|
||||
character.digestionRateMax = json_float(root, "digestionRateMax", character.digestionRate * TICK_RATE) / TICK_RATE;
|
||||
character.digestionTime = json_int(root, "digestionTime");
|
||||
character.blinkChance = json_float(root, "blinkChance");
|
||||
character.gurgleChance = json_float(root, "gurgleChance");
|
||||
character.gurgleCapacityMultiplier = json_float(root, "gurgleCapacityMultiplier");
|
||||
Traits& traits = character.traits;
|
||||
traits.weightMax = json_float(root, "weightMax", traits.weight);
|
||||
traits.capacity = json_float(root, "capacity");
|
||||
traits.capacityMin = json_float(root, "capacityMin");
|
||||
traits.capacityMax = json_float(root, "capacityMax", traits.capacity);
|
||||
traits.overstuffedMultiplier = json_float(root, "overstuffedMultiplier", 1);
|
||||
traits.overstuffedDigestBonus = json_float(root, "overstuffedDigestBonus");
|
||||
traits.caloriesToKilogram = json_float(root, "caloriesToKilogram");
|
||||
traits.digestionRate = json_float(root, "digestionRate") / TICK_RATE;
|
||||
traits.digestionRateMin = json_float(root, "digestionRateMin") / TICK_RATE;
|
||||
traits.digestionRateMax = json_float(root, "digestionRateMax", traits.digestionRate * TICK_RATE) / TICK_RATE;
|
||||
traits.digestionTime = json_int(root, "digestionTime");
|
||||
traits.blinkChance = json_float(root, "blinkChance");
|
||||
traits.gurgleChance = json_float(root, "gurgleChance");
|
||||
traits.gurgleCapacityMultiplier = json_float(root, "gurgleCapacityMultiplier");
|
||||
character.dialoguePool = json_string(root, "dialoguePool");
|
||||
|
||||
object alternate = json_object(root, "alternate");
|
||||
if (!alternate["texture"].error())
|
||||
character.alternateTexture = renderer.texture_get(character.folder + json_string(alternate, "texture"));
|
||||
character.alternate.texture = renderer.texture_get(character.folder + json_string(alternate, "texture"));
|
||||
if (!alternate["sound"].error())
|
||||
character.alternateSound = audio.sound_get(character.folder + json_string(alternate, "sound"));
|
||||
character.alternateChance = json_float(alternate, "chance");
|
||||
character.alternateSheet = json_int(alternate, "spritesheet");
|
||||
character.alternate.sound = audio.sound_get(character.folder + json_string(alternate, "sound"));
|
||||
character.alternate.chance = json_float(alternate, "chance");
|
||||
character.alternate.sheet = json_int(alternate, "spritesheet");
|
||||
|
||||
object animations = json_object(root, "animations");
|
||||
character.animationStart = json_string(animations, "start");
|
||||
character.animationIdle = json_string(animations, "idle");
|
||||
character.animationIdleFull = json_string(animations, "idleFull");
|
||||
character.animationStageUp = json_string(animations, "stageUp");
|
||||
character.animations = {json_string(animations, "idle"), json_string(animations, "idleFull"),
|
||||
json_string(animations, "stageUp"), json_string(animations, "start")};
|
||||
|
||||
Anm2* anm2 = character.anm2;
|
||||
auto layer_get = [&](object source, const char* key) {
|
||||
@@ -157,10 +171,9 @@ 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.talkSource = layer_get(json_object(root, "talk"), "source");
|
||||
character.talkDestination = layer_get(json_object(root, "talk"), "destination");
|
||||
character.blinkSource = layer_get(json_object(root, "blink"), "source");
|
||||
character.blinkDestination = layer_get(json_object(root, "blink"), "destination");
|
||||
character.talk = {layer_get(json_object(root, "talk"), "destination"), layer_get(json_object(root, "talk"), "source")};
|
||||
character.blink = {layer_get(json_object(root, "blink"), "destination"),
|
||||
layer_get(json_object(root, "blink"), "source")};
|
||||
|
||||
for (element entry : json_array(root, "eatAreas"))
|
||||
{
|
||||
@@ -225,26 +238,28 @@ static void character_parse(object root, Character& character, Renderer& rendere
|
||||
}
|
||||
|
||||
object cursor = json_object(root, "cursor");
|
||||
character.cursor = character.anm2_get(json_string(cursor, "anm2"), renderer, audio);
|
||||
object cursorAnimations = json_object(cursor, "animations");
|
||||
character.cursorIdle = json_string(cursorAnimations, "idle");
|
||||
character.cursorClick = json_string(cursorAnimations, "click");
|
||||
character.cursorHover = json_string(cursorAnimations, "hover");
|
||||
character.cursorGrab = json_string(cursorAnimations, "grab");
|
||||
character.cursorPan = json_string(cursorAnimations, "pan");
|
||||
character.cursorZoom = json_string(cursorAnimations, "zoom");
|
||||
character.cursorDispose = json_string(cursorAnimations, "dispose");
|
||||
character.grabParticles = json_strings(json_object(cursor, "particles"), "grab");
|
||||
auto cursor_get = [&](const char* key) {
|
||||
return json_string(cursorAnimations, key);
|
||||
};
|
||||
character.cursor = {cursor_get("click"),
|
||||
cursor_get("dispose"),
|
||||
cursor_get("grab"),
|
||||
cursor_get("hover"),
|
||||
cursor_get("idle"),
|
||||
cursor_get("pan"),
|
||||
cursor_get("zoom"),
|
||||
json_strings(json_object(cursor, "particles"), "grab"),
|
||||
character.anm2_get(json_string(cursor, "anm2"), renderer, audio)};
|
||||
|
||||
object menu = json_object(root, "menu");
|
||||
ImFontAtlas* fonts = ImGui::GetIO().Fonts;
|
||||
if (!menu["font"].error())
|
||||
character.font = fonts->AddFontFromFileTTF((character.folder + json_string(menu, "font")).c_str(), FONT_SIZE);
|
||||
if (!menu["iconsFont"].error())
|
||||
character.iconsFont =
|
||||
fonts->AddFontFromFileTTF((character.folder + json_string(menu, "iconsFont")).c_str(), FONT_SIZE);
|
||||
character.rounding = json_float(menu, "rounding");
|
||||
character.homeGlyph = json_string(menu, "homeGlyph");
|
||||
auto font_get = [&](const char* key) {
|
||||
std::string path = json_string(menu, key);
|
||||
return path.empty() ? nullptr
|
||||
: ImGui::GetIO().Fonts->AddFontFromFileTTF((character.folder + path).c_str(), FONT_SIZE);
|
||||
};
|
||||
character.menu = {json_string(menu, "homeGlyph"), font_get("font"), font_get("iconsFont"),
|
||||
json_float(menu, "rounding")};
|
||||
|
||||
for (auto [name, value] : json_object(root, "particles"))
|
||||
{
|
||||
@@ -253,22 +268,19 @@ static void character_parse(object root, Character& character, Renderer& rendere
|
||||
character.particles[std::string(name)] = {
|
||||
json_string(fields, "animation"), character.anm2_get(json_string(fields, "anm2"), renderer, audio)};
|
||||
}
|
||||
|
||||
object strings = json_object(root, "strings");
|
||||
for (int i = 0; i < STRING_COUNT; i++)
|
||||
character.strings[i] = json_string(strings, STRING_NAMES[i], STRING_DEFAULTS[i]);
|
||||
}
|
||||
|
||||
static void items_parse(object root, Character& character, Renderer& renderer, Audio& audio)
|
||||
{
|
||||
Catalog& catalog = character.catalog;
|
||||
Anm2* baseAnm2 = character.anm2_get(json_string(root, "anm2"), renderer, audio);
|
||||
character.itemSheet = json_int(root, "spritesheet");
|
||||
character.spawnDelay = json_int(root, "spawnDelay", character.spawnDelay);
|
||||
character.spawnDelayMultiplier = json_float(root, "spawnDelayMultiplier", character.spawnDelayMultiplier);
|
||||
character.spawnLimit = json_int(root, "spawnLimit", character.spawnLimit);
|
||||
catalog.sheet = json_int(root, "spritesheet");
|
||||
catalog.spawnDelay = json_int(root, "spawnDelay", catalog.spawnDelay);
|
||||
catalog.spawnDelayMultiplier = json_float(root, "spawnDelayMultiplier", catalog.spawnDelayMultiplier);
|
||||
catalog.spawnLimit = json_int(root, "spawnLimit", catalog.spawnLimit);
|
||||
object particles = json_object(root, "particles");
|
||||
character.disposeParticles = json_strings(particles, "dispose");
|
||||
character.summonParticles = json_strings(particles, "summon");
|
||||
catalog.disposeParticles = json_strings(particles, "dispose");
|
||||
catalog.summonParticles = json_strings(particles, "summon");
|
||||
|
||||
object categories = json_object(root, "categories");
|
||||
std::vector<std::string> rarityNames;
|
||||
@@ -278,7 +290,7 @@ static void items_parse(object root, Character& character, Renderer& renderer, A
|
||||
if (value.get(fields))
|
||||
continue;
|
||||
std::string sound = json_string(fields, "sound");
|
||||
character.rarities.push_back({std::string(name), sound.empty() ? nullptr : audio.sound_get(character.folder + sound),
|
||||
catalog.rarities.push_back({std::string(name), sound.empty() ? nullptr : audio.sound_get(character.folder + sound),
|
||||
json_float(fields, "weight")});
|
||||
rarityNames.emplace_back(name);
|
||||
}
|
||||
@@ -288,7 +300,7 @@ static void items_parse(object root, Character& character, Renderer& renderer, A
|
||||
object fields;
|
||||
if (entry.get(fields))
|
||||
continue;
|
||||
Item& item = character.items.emplace_back();
|
||||
Item& item = catalog.items.emplace_back();
|
||||
item.name = json_string(fields, "name", "???");
|
||||
item.description = json_string(fields, "description", "???");
|
||||
item.category = json_string(fields, "category");
|
||||
@@ -323,12 +335,9 @@ static void dialogue_parse(object root, Character& character, Audio& audio)
|
||||
if (!value.get(fields))
|
||||
dialogue.pauses[std::string(letter)] = {json_int(fields, "delay"), json_is(fields, "isSilent")};
|
||||
}
|
||||
for (auto [hook, value] : json_object(root, "hooks"))
|
||||
{
|
||||
std::string_view target;
|
||||
if (!value.get(target))
|
||||
dialogue.hooks[std::string(hook)] = target;
|
||||
}
|
||||
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;
|
||||
@@ -418,10 +427,11 @@ void Character::load(Renderer& renderer, Audio& audio)
|
||||
// An actor for an item, showing its texture in place of its anm2's item spritesheet.
|
||||
Actor Character::item_actor_get(int item)
|
||||
{
|
||||
Actor actor(items[item].anm2);
|
||||
Item& entry = catalog.items[item];
|
||||
Actor actor(entry.anm2);
|
||||
actor.isStatic = true;
|
||||
if (items[item].texture && itemSheet >= 0 && itemSheet < (int)actor.sheets.size())
|
||||
actor.sheets[itemSheet] = items[item].texture;
|
||||
if (entry.texture && catalog.sheet >= 0 && catalog.sheet < (int)actor.sheets.size())
|
||||
actor.sheets[catalog.sheet] = entry.texture;
|
||||
return actor;
|
||||
}
|
||||
|
||||
|
||||
+125
-62
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "anm2.h"
|
||||
#include "strings.h"
|
||||
#include <array>
|
||||
#include <unordered_map>
|
||||
|
||||
@@ -38,6 +37,32 @@ enum SoundId
|
||||
SOUND_COUNT
|
||||
};
|
||||
|
||||
// The game events dialogue can hook onto, by their names in dialogue.json.
|
||||
#define HOOKS(X) \
|
||||
X(START, "Start") \
|
||||
X(END, "End") \
|
||||
X(HELP, "Help") \
|
||||
X(RANDOM, "Random") \
|
||||
X(STAGE_UP, "StageUp") \
|
||||
X(DIGEST, "Digest") \
|
||||
X(FEED, "Feed") \
|
||||
X(FEED_FULL, "FeedFull") \
|
||||
X(EAT, "Eat") \
|
||||
X(EAT_FULL, "EatFull") \
|
||||
X(FULL, "Full") \
|
||||
X(LOW_CAPACITY, "LowCapacity") \
|
||||
X(THROW, "Throw") \
|
||||
X(FOOD_TAKEN, "FoodTaken") \
|
||||
X(FOOD_TAKEN_FULL, "FoodTakenFull")
|
||||
|
||||
enum Hook
|
||||
{
|
||||
#define X(id, name) HOOK_##id,
|
||||
HOOKS(X)
|
||||
#undef X
|
||||
HOOK_COUNT
|
||||
};
|
||||
|
||||
struct Stage
|
||||
{
|
||||
std::string animationSuffix;
|
||||
@@ -187,13 +212,12 @@ struct Pause
|
||||
bool isSilent = false;
|
||||
};
|
||||
|
||||
// Dialogue entries by label, pools of them picked from at random, and hooks naming the pool (or else entry) the game
|
||||
// plays on an event.
|
||||
// Dialogue entries by label, pools of them picked from at random, and the pool (or else entry) each hook plays.
|
||||
struct Dialogue
|
||||
{
|
||||
std::array<std::string, HOOK_COUNT> hooks;
|
||||
std::unordered_map<std::string, Entry> entries;
|
||||
std::unordered_map<std::string, Pause> pauses;
|
||||
std::unordered_map<std::string, std::string> hooks;
|
||||
std::unordered_map<std::string, std::vector<std::string>> pools;
|
||||
Sounds blip;
|
||||
int blipDelay = 0;
|
||||
@@ -201,59 +225,13 @@ struct Dialogue
|
||||
|
||||
Entry* entry_get(const std::string& label);
|
||||
Entry* pool_get(const std::string& pool);
|
||||
Entry* hook_get(const std::string& hook);
|
||||
Entry* hook_get(Hook hook);
|
||||
bool is_hook(Hook hook);
|
||||
};
|
||||
|
||||
// Everything a character folder holds. The preview (what the select screen shows) loads at startup, the rest when
|
||||
// played.
|
||||
class Character
|
||||
// The body a character starts with and the limits it grows within. Rates are percent per tick.
|
||||
struct Traits
|
||||
{
|
||||
public:
|
||||
std::array<Sounds, SOUND_COUNT> sounds;
|
||||
std::array<std::string, STRING_COUNT> strings;
|
||||
std::string animationIdle;
|
||||
std::string animationIdleFull;
|
||||
std::string animationStageUp;
|
||||
std::string animationStart;
|
||||
std::string anm2Path;
|
||||
std::string credits;
|
||||
std::string cursorClick;
|
||||
std::string cursorDispose;
|
||||
std::string cursorGrab;
|
||||
std::string cursorHover;
|
||||
std::string cursorIdle;
|
||||
std::string cursorPan;
|
||||
std::string cursorZoom;
|
||||
std::string description;
|
||||
std::string dialoguePool;
|
||||
std::string folder;
|
||||
std::string homeGlyph;
|
||||
std::string key;
|
||||
std::string name;
|
||||
std::unordered_map<std::string, Anm2> anm2s;
|
||||
std::unordered_map<std::string, Particle> particles;
|
||||
std::vector<std::string> disposeParticles;
|
||||
std::vector<std::string> grabParticles;
|
||||
std::vector<std::string> summonParticles;
|
||||
std::vector<Area> areas;
|
||||
std::vector<EatArea> eatAreas;
|
||||
std::vector<ExpandArea> expandAreas;
|
||||
std::vector<InteractArea> interactAreas;
|
||||
std::vector<InteractArea> interactTypes;
|
||||
std::vector<Item> items;
|
||||
std::vector<Rarity> rarities;
|
||||
std::vector<Stage> stages;
|
||||
Dialogue dialogue;
|
||||
Anm2* anm2 = nullptr;
|
||||
Anm2* cursor = nullptr;
|
||||
ImFont* font = nullptr;
|
||||
ImFont* iconsFont = nullptr;
|
||||
Sound* alternateSound = nullptr;
|
||||
Texture* alternateTexture = nullptr;
|
||||
Texture* portrait = nullptr;
|
||||
Texture* render = nullptr;
|
||||
Color color = {0.5f, 0.5f, 0.5f, 1};
|
||||
float alternateChance = 0;
|
||||
float blinkChance = 0;
|
||||
float caloriesToKilogram = 0;
|
||||
float capacity = 0;
|
||||
@@ -266,19 +244,104 @@ public:
|
||||
float gurgleChance = 0;
|
||||
float overstuffedDigestBonus = 0;
|
||||
float overstuffedMultiplier = 1;
|
||||
float rounding = 0;
|
||||
float spawnDelayMultiplier = 1;
|
||||
float weight = 0;
|
||||
float weightMax = 0;
|
||||
int alternateSheet = 0;
|
||||
int blinkDestination = -1;
|
||||
int blinkSource = -1;
|
||||
int digestionTime = 0;
|
||||
int itemSheet = 0;
|
||||
};
|
||||
|
||||
struct Alternate
|
||||
{
|
||||
Sound* sound = nullptr;
|
||||
Texture* texture = nullptr;
|
||||
float chance = 0;
|
||||
int sheet = 0;
|
||||
};
|
||||
|
||||
struct Animations
|
||||
{
|
||||
std::string idle;
|
||||
std::string idleFull;
|
||||
std::string stageUp;
|
||||
std::string start;
|
||||
};
|
||||
|
||||
// A layer whose region is shown on another: for talking and blinking.
|
||||
struct Swap
|
||||
{
|
||||
int destination = -1;
|
||||
int source = -1;
|
||||
};
|
||||
|
||||
struct Cursor
|
||||
{
|
||||
std::string click;
|
||||
std::string dispose;
|
||||
std::string grab;
|
||||
std::string hover;
|
||||
std::string idle;
|
||||
std::string pan;
|
||||
std::string zoom;
|
||||
std::vector<std::string> grabParticles;
|
||||
Anm2* anm2 = nullptr;
|
||||
};
|
||||
|
||||
struct Menu
|
||||
{
|
||||
std::string homeGlyph;
|
||||
ImFont* font = nullptr;
|
||||
ImFont* iconsFont = nullptr;
|
||||
float rounding = 0;
|
||||
};
|
||||
|
||||
// The items a character can be given, their rarities, and how often they spawn.
|
||||
struct Catalog
|
||||
{
|
||||
std::vector<std::string> disposeParticles;
|
||||
std::vector<std::string> summonParticles;
|
||||
std::vector<Item> items;
|
||||
std::vector<Rarity> rarities;
|
||||
float spawnDelayMultiplier = 1;
|
||||
int sheet = 0;
|
||||
int spawnDelay = 60;
|
||||
int spawnLimit = 100;
|
||||
int talkDestination = -1;
|
||||
int talkSource = -1;
|
||||
|
||||
int item_get(const std::string& name);
|
||||
};
|
||||
|
||||
// Everything a character folder holds. The preview (what the select screen shows) loads at startup, the rest when
|
||||
// played.
|
||||
class Character
|
||||
{
|
||||
public:
|
||||
std::array<Sounds, SOUND_COUNT> sounds;
|
||||
std::string anm2Path;
|
||||
std::string credits;
|
||||
std::string description;
|
||||
std::string dialoguePool;
|
||||
std::string folder;
|
||||
std::string key;
|
||||
std::string name;
|
||||
std::unordered_map<std::string, Anm2> anm2s;
|
||||
std::unordered_map<std::string, Particle> particles;
|
||||
std::vector<Area> areas;
|
||||
std::vector<EatArea> eatAreas;
|
||||
std::vector<ExpandArea> expandAreas;
|
||||
std::vector<InteractArea> interactAreas;
|
||||
std::vector<InteractArea> interactTypes;
|
||||
std::vector<Stage> stages;
|
||||
Alternate alternate;
|
||||
Animations animations;
|
||||
Catalog catalog;
|
||||
Cursor cursor;
|
||||
Dialogue dialogue;
|
||||
Menu menu;
|
||||
Swap blink;
|
||||
Swap talk;
|
||||
Traits traits;
|
||||
Anm2* anm2 = nullptr;
|
||||
Texture* portrait = nullptr;
|
||||
Texture* render = nullptr;
|
||||
Color color = {0.5f, 0.5f, 0.5f, 1};
|
||||
bool isLoaded = false;
|
||||
bool isValid = false;
|
||||
|
||||
|
||||
+24
-27
@@ -5,14 +5,14 @@
|
||||
constexpr float CONSOLE_HEIGHT = 0.4f;
|
||||
constexpr int INPUT_SIZE = 256;
|
||||
|
||||
struct Command
|
||||
struct ConsoleCommand
|
||||
{
|
||||
const char* name;
|
||||
const char* usage;
|
||||
bool isPlayOnly;
|
||||
};
|
||||
|
||||
constexpr Command COMMANDS[] = {
|
||||
constexpr ConsoleCommand COMMANDS[] = {
|
||||
{"animations", "animations", true},
|
||||
{"capacity", "capacity <calories>", true},
|
||||
{"character", "character <name>", false},
|
||||
@@ -66,7 +66,7 @@ void Console::execute(Play* play, std::vector<Character>& characters)
|
||||
inputs.push_back(input);
|
||||
inputIndex = -1;
|
||||
history.push_back("> " + input);
|
||||
auto command = std::find_if(std::begin(COMMANDS), std::end(COMMANDS), [&](const Command& entry) {
|
||||
auto command = std::find_if(std::begin(COMMANDS), std::end(COMMANDS), [&](const ConsoleCommand& entry) {
|
||||
return name == entry.name;
|
||||
});
|
||||
if (command == std::end(COMMANDS))
|
||||
@@ -74,21 +74,18 @@ void Console::execute(Play* play, std::vector<Character>& characters)
|
||||
if (command->isPlayOnly && !play)
|
||||
return history.push_back("Command unavailable outside game: " + name);
|
||||
float number = (float)SDL_atof(argument.c_str());
|
||||
Progress* progress = play ? &play->progress : nullptr;
|
||||
Body* body = play ? &play->body : nullptr;
|
||||
|
||||
if (name == "animations")
|
||||
{
|
||||
Anm2* anm2 = play->actor.anm2;
|
||||
Anm2* anm2 = body->actor.anm2;
|
||||
for (int i = 0; anm2 && i < (int)anm2->animations.size(); i++)
|
||||
history.push_back(" " + std::to_string(i) + " " + anm2->animations[i].name);
|
||||
}
|
||||
else if (name == "capacity")
|
||||
progress->capacity = number;
|
||||
body->stats.capacity = number;
|
||||
else if (name == "character" || name == "new_game")
|
||||
{
|
||||
action = name == "character" ? ACTION_CHARACTER : ACTION_NEW_GAME;
|
||||
target = argument;
|
||||
}
|
||||
commands->push_back({argument, name == "character" ? COMMAND_CONTINUE : COMMAND_NEW_GAME});
|
||||
else if (name == "characters")
|
||||
{
|
||||
for (Character& character : characters)
|
||||
@@ -101,53 +98,53 @@ void Console::execute(Play* play, std::vector<Character>& characters)
|
||||
}
|
||||
else if (name == "digest")
|
||||
{
|
||||
progress->isDigesting = true;
|
||||
progress->digestionTimer = 1;
|
||||
body->stats.isDigesting = true;
|
||||
body->stats.digestionTimer = 1;
|
||||
}
|
||||
else if (name == "digestion")
|
||||
progress->digestionRate = number / TICK_RATE;
|
||||
body->stats.digestionRate = number / TICK_RATE;
|
||||
else if (name == "help")
|
||||
{
|
||||
for (const Command& entry : COMMANDS)
|
||||
for (const ConsoleCommand& entry : COMMANDS)
|
||||
if (play || !entry.isPlayOnly)
|
||||
history.push_back(std::string(" ") + entry.usage);
|
||||
}
|
||||
else if (name == "items")
|
||||
{
|
||||
for (int i = 0; i < (int)play->character->items.size(); i++)
|
||||
history.push_back(" " + std::to_string(i) + " " + play->character->items[i].name);
|
||||
std::vector<Item>& items = play->character->catalog.items;
|
||||
for (int i = 0; i < (int)items.size(); i++)
|
||||
history.push_back(" " + std::to_string(i) + " " + items[i].name);
|
||||
}
|
||||
else if (name == "menu")
|
||||
action = ACTION_MENU;
|
||||
commands->push_back({"", COMMAND_MENU});
|
||||
else if (name == "play_animation")
|
||||
play->actor.play(argument, TRANSITION_FORCE, INTERRUPT_ALWAYS);
|
||||
body->actor.play(argument, TRANSITION_FORCE, INTERRUPT_ALWAYS);
|
||||
else if (name == "play_dialogue")
|
||||
play->text.set(*play, play->character->dialogue.entry_get(argument));
|
||||
else if (name == "reload")
|
||||
action = ACTION_RELOAD;
|
||||
commands->push_back({"", COMMAND_RELOAD});
|
||||
else if (name == "show_layers")
|
||||
Actor::isShowLayers = is_toggled(argument, Actor::isShowLayers);
|
||||
renderer->isShowLayers = is_toggled(argument, renderer->isShowLayers);
|
||||
else if (name == "show_nulls")
|
||||
Actor::isShowNulls = is_toggled(argument, Actor::isShowNulls);
|
||||
renderer->isShowNulls = is_toggled(argument, renderer->isShowNulls);
|
||||
else if (name == "spawn")
|
||||
{
|
||||
int id = SDL_atoi(argument.c_str());
|
||||
if (argument.empty() || id < 0 || id >= (int)play->character->items.size())
|
||||
if (argument.empty() || id < 0 || id >= (int)play->character->catalog.items.size())
|
||||
history.push_back("Unable to spawn item " + argument + ".");
|
||||
else
|
||||
play->spawn_queue_add(id);
|
||||
play->items.spawn_queue_add(*play, id);
|
||||
}
|
||||
else if (name == "stage" || name == "weight")
|
||||
{
|
||||
play->stage = name == "stage" ? std::max(0, SDL_atoi(argument.c_str()) - 1) : play->stage_get(number);
|
||||
progress->weight = name == "stage" ? play->stage_threshold_get(play->stage) : number;
|
||||
play->idle_play();
|
||||
body->stage_set(name == "stage" ? std::max(0, SDL_atoi(argument.c_str()) - 1) : body->stage_get(number));
|
||||
body->stats.weight = name == "stage" ? body->stage_threshold_get(body->stage) : number;
|
||||
body->idle_play();
|
||||
}
|
||||
}
|
||||
|
||||
void Console::update(Play* play, std::vector<Character>& characters)
|
||||
{
|
||||
action = ACTION_NONE;
|
||||
if (!isOpen)
|
||||
return;
|
||||
ImVec2 screen = ImGui::GetIO().DisplaySize;
|
||||
|
||||
+4
-12
@@ -1,26 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "play.h"
|
||||
|
||||
enum ConsoleAction
|
||||
{
|
||||
ACTION_NONE,
|
||||
ACTION_CHARACTER,
|
||||
ACTION_MENU,
|
||||
ACTION_NEW_GAME,
|
||||
ACTION_RELOAD
|
||||
};
|
||||
#include "ui.h"
|
||||
|
||||
// A developer console (opened with the grave key when "console=1" is in settings.txt) for inspecting and changing a
|
||||
// game. Commands that change characters are left as an action for the game to carry out.
|
||||
// game. Commands that change what's running go to the game's command list.
|
||||
class Console
|
||||
{
|
||||
public:
|
||||
std::vector<std::string> history;
|
||||
std::vector<std::string> inputs;
|
||||
std::string input;
|
||||
std::string target;
|
||||
ConsoleAction action = ACTION_NONE;
|
||||
Commands* commands = nullptr;
|
||||
Renderer* renderer = nullptr;
|
||||
int inputIndex = -1;
|
||||
bool isOpen = false;
|
||||
|
||||
|
||||
+106
-287
@@ -1,151 +1,33 @@
|
||||
#include "dialogue.h"
|
||||
#include "play.h"
|
||||
#include "ui.h"
|
||||
#include <cfloat>
|
||||
#include <imgui_internal.h>
|
||||
|
||||
constexpr float LIFE_MAX = 10;
|
||||
constexpr float OSCILLATION_AMPLITUDE = 2;
|
||||
constexpr float OSCILLATION_FREQUENCY = 1;
|
||||
constexpr float OSCILLATION_SPREAD = 0.16f;
|
||||
constexpr float SCREENSHAKE_MAGNITUDE = 0.03f;
|
||||
constexpr int SCREENSHAKE_TIME = 15;
|
||||
constexpr float SHAKE_AMPLITUDE = 1;
|
||||
constexpr float SHAKE_FREQUENCY = 30;
|
||||
constexpr float TEXT_BIG = 1.25f;
|
||||
constexpr float TEXT_SMALL = 0.75f;
|
||||
constexpr float WIDTH_MULTIPLIER = 0.3f;
|
||||
constexpr float HEIGHT_LINES = 6;
|
||||
constexpr float INDICATOR_BOB = 0.12f;
|
||||
constexpr float INDICATOR_SIZE = 0.35f;
|
||||
constexpr float INDICATOR_SPEED = 2;
|
||||
|
||||
static bool is_effect_on(const Effect& effect, int at)
|
||||
{
|
||||
return effect.start <= at && at < effect.end;
|
||||
}
|
||||
|
||||
static bool is_silent(const Entry& entry, int at)
|
||||
{
|
||||
for (const Effect& effect : entry.effects)
|
||||
if (effect.type == EFFECT_SILENT && is_effect_on(effect, at))
|
||||
if (effect.type == EFFECT_SILENT && effect.start <= at && at < effect.end)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static float hash_unit(Uint32 value)
|
||||
static bool is_letter_start(const std::string& text, size_t at)
|
||||
{
|
||||
value ^= value >> 16;
|
||||
value *= 0x7feb352du;
|
||||
value ^= value >> 15;
|
||||
value *= 0x846ca68bu;
|
||||
value ^= value >> 16;
|
||||
return (value & 0xffffu) / 65535.0f;
|
||||
return at >= text.size() || ((unsigned char)text[at] & 0xC0) != 0x80;
|
||||
}
|
||||
|
||||
// The scale, color and offset the 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 UTF-8 character at an index of a text, or empty past its end.
|
||||
static std::string letter_get(const std::string& text, int at)
|
||||
{
|
||||
float scale = 1;
|
||||
color = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
offset = {};
|
||||
for (const Effect& effect : entry.effects)
|
||||
{
|
||||
if (!is_effect_on(effect, at))
|
||||
continue;
|
||||
Uint32 seed = (Uint32)std::floor(time * SHAKE_FREQUENCY) * 747796405u + (Uint32)at * 2891336453u;
|
||||
switch (effect.type)
|
||||
{
|
||||
case EFFECT_COLOR:
|
||||
color = ImGui::GetColorU32(imvec4_get({effect.color.r, effect.color.g, effect.color.b, 1}));
|
||||
break;
|
||||
case EFFECT_BIG:
|
||||
scale = TEXT_BIG;
|
||||
break;
|
||||
case EFFECT_SMALL:
|
||||
scale = TEXT_SMALL;
|
||||
break;
|
||||
case EFFECT_OSCILLATION:
|
||||
offset.y += std::sin((time * OSCILLATION_FREQUENCY + at * OSCILLATION_SPREAD) * 2 * PI) *
|
||||
OSCILLATION_AMPLITUDE;
|
||||
break;
|
||||
case EFFECT_SHAKE:
|
||||
offset.x += (hash_unit(seed) * 2 - 1) * SHAKE_AMPLITUDE;
|
||||
offset.y += (hash_unit(seed ^ 0xa511e9b3u) * 2 - 1) * SHAKE_AMPLITUDE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
// Draws the 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)
|
||||
{
|
||||
ImFont* font = ImGui::GetFont();
|
||||
float fontSize = ImGui::GetFontSize();
|
||||
ImVec2 start = ImGui::GetCursorScreenPos();
|
||||
float wrap = start.x + ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 cursor = start;
|
||||
float lineHeight = fontSize;
|
||||
const char* end = entry.text.data() + entry.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;
|
||||
};
|
||||
auto line_break = [&]() {
|
||||
cursor = {start.x, cursor.y + lineHeight};
|
||||
lineHeight = fontSize;
|
||||
};
|
||||
|
||||
int at = 0;
|
||||
for (const char* it = entry.text.data(); it < end && at < count; at++)
|
||||
{
|
||||
unsigned int codepoint = 0;
|
||||
const char* next = it + ImTextCharFromUtf8(&codepoint, it, end);
|
||||
if (codepoint == '\n')
|
||||
{
|
||||
line_break();
|
||||
it = next;
|
||||
continue;
|
||||
}
|
||||
bool isSpace = codepoint == ' ';
|
||||
if (!isSpace && cursor.x > start.x && it[-1] == ' ')
|
||||
{
|
||||
float wordWidth = 0;
|
||||
int wordAt = at;
|
||||
for (const char* letter = it; letter < end && *letter != ' ' && *letter != '\n'; wordAt++)
|
||||
{
|
||||
unsigned int wordCodepoint = 0;
|
||||
const char* letterNext = letter + ImTextCharFromUtf8(&wordCodepoint, letter, end);
|
||||
wordWidth += width_get(letter, letterNext, wordAt);
|
||||
letter = letterNext;
|
||||
}
|
||||
if (cursor.x + wordWidth > wrap && wordWidth <= wrap - start.x)
|
||||
line_break();
|
||||
}
|
||||
ImU32 color;
|
||||
ImVec2 offset;
|
||||
float scale = letter_style_get(entry, at, time, color, offset);
|
||||
float width = width_get(it, next, at);
|
||||
if (cursor.x + width > wrap)
|
||||
{
|
||||
line_break();
|
||||
if (isSpace)
|
||||
{
|
||||
it = next;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ImGui::GetWindowDrawList()->AddText(font, fontSize * scale, {cursor.x + offset.x, cursor.y + offset.y}, color,
|
||||
it, next);
|
||||
cursor.x += width;
|
||||
lineHeight = std::max(lineHeight, fontSize * scale);
|
||||
it = next;
|
||||
}
|
||||
ImGui::Dummy({wrap - start.x, cursor.y + lineHeight - start.y});
|
||||
size_t start = 0;
|
||||
while (start < text.size() && at > 0)
|
||||
at -= is_letter_start(text, ++start);
|
||||
size_t end = std::min(start + 1, text.size());
|
||||
while (!is_letter_start(text, end))
|
||||
end++;
|
||||
return text.substr(start, end - start);
|
||||
}
|
||||
|
||||
void Text::set(Play& play, Entry* next, TextMode mode, const std::string& onEnd)
|
||||
@@ -169,54 +51,50 @@ void Text::set(Play& play, Entry* next, TextMode mode, const std::string& onEnd)
|
||||
Interrupt interrupt = mode == TEXT_SEQUENCE ? INTERRUPT_ALWAYS
|
||||
: mode == TEXT_INTERRUPTIBLE ? INTERRUPT_IF_ALLOWED
|
||||
: INTERRUPT_NEVER;
|
||||
if (!next->animation.empty())
|
||||
play.body_play(next->animation, TRANSITION_NORMAL, interrupt);
|
||||
delayBase = play.character->dialogue.delay;
|
||||
delay = delayBase;
|
||||
play.body.play(next->animation, TRANSITION_NORMAL, interrupt);
|
||||
delay = delayBase = play.character->dialogue.delay;
|
||||
for (Effect& effect : next->effects)
|
||||
{
|
||||
if (effect.start != 0)
|
||||
continue;
|
||||
if (effect.type == EFFECT_DELAY)
|
||||
delay = delayBase = std::max(1, effect.delay);
|
||||
if (effect.type == EFFECT_ANIMATION && !effect.animation.empty())
|
||||
play.body_play(effect.animation, TRANSITION_FORCE);
|
||||
if (effect.type == EFFECT_ANIMATION)
|
||||
play.body.play(effect.animation, TRANSITION_FORCE);
|
||||
}
|
||||
if (!next->text.empty())
|
||||
play.isTalking = !is_silent(*next, 0);
|
||||
}
|
||||
|
||||
// Applies an effect as its character comes up: a delay or animation, a screen shake or a sound.
|
||||
static void effect_apply(Play& play, Text& text, const Effect& effect)
|
||||
{
|
||||
if (effect.type == EFFECT_DELAY)
|
||||
text.delay = text.delayBase = std::max(1, effect.delay);
|
||||
else if (effect.type == EFFECT_ANIMATION && !effect.animation.empty())
|
||||
play.body_play(effect.animation, TRANSITION_FORCE);
|
||||
else if (effect.type == EFFECT_SCREENSHAKE)
|
||||
play.shake_add(SCREENSHAKE_MAGNITUDE, SCREENSHAKE_TIME);
|
||||
else if (effect.type == EFFECT_SOUND)
|
||||
play.audio->play(effect.sound);
|
||||
play.body.isTalking = !is_silent(*next, 0);
|
||||
}
|
||||
|
||||
// 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)
|
||||
if (effect.start == at)
|
||||
effect_apply(play, *this, effect);
|
||||
{
|
||||
if (effect.start != at)
|
||||
continue;
|
||||
if (effect.type == EFFECT_DELAY)
|
||||
delay = delayBase = std::max(1, effect.delay);
|
||||
else if (effect.type == EFFECT_ANIMATION)
|
||||
play.body.play(effect.animation, TRANSITION_FORCE);
|
||||
else if (effect.type == EFFECT_SCREENSHAKE)
|
||||
play.camera.shake_add(SCREENSHAKE_MAGNITUDE, SCREENSHAKE_TIME);
|
||||
else if (effect.type == EFFECT_SOUND)
|
||||
play.audio->play(effect.sound);
|
||||
}
|
||||
}
|
||||
|
||||
void Text::clear(Play& play)
|
||||
{
|
||||
if (!idleOnEnd.empty())
|
||||
{
|
||||
play.idle_override_set(idleOnEnd);
|
||||
play.idle_play();
|
||||
play.body.idle_override_set(idleOnEnd);
|
||||
play.body.idle_play();
|
||||
idleOnEnd.clear();
|
||||
}
|
||||
entry = nullptr;
|
||||
isScene = false;
|
||||
play.isTalking = false;
|
||||
play.body.isTalking = false;
|
||||
}
|
||||
|
||||
bool Text::is_interruptible()
|
||||
@@ -224,157 +102,98 @@ bool Text::is_interruptible()
|
||||
return !entry || entry->isTerminal;
|
||||
}
|
||||
|
||||
void Text::update(Play& play, Vec2 screenSize)
|
||||
// The player's click or key: shows the rest of the line, or else moves to the next entry or closes the text.
|
||||
void Text::advance(Play& play)
|
||||
{
|
||||
if (!entry)
|
||||
return;
|
||||
if (!isFinished)
|
||||
{
|
||||
for (int skipped = index + 1; skipped <= entry->length; skipped++)
|
||||
effects_apply(play, skipped);
|
||||
index = entry->length;
|
||||
isFinished = true;
|
||||
play.body.isTalking = false;
|
||||
isScene &= !entry->isTerminal;
|
||||
}
|
||||
else if (!entry->choices.empty() || !entry->next.empty())
|
||||
{
|
||||
play.audio->play(play.character->sounds[SOUND_SELECT]);
|
||||
if (!entry->choices.empty())
|
||||
choose(play, 0);
|
||||
else
|
||||
set(play, play.character->dialogue.entry_get(entry->next), TEXT_SEQUENCE);
|
||||
}
|
||||
else
|
||||
clear(play);
|
||||
}
|
||||
|
||||
void Text::choose(Play& play, int choice)
|
||||
{
|
||||
if (entry && choice >= 0 && choice < (int)entry->choices.size())
|
||||
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.
|
||||
void Text::update(Play& play)
|
||||
{
|
||||
if (!entry)
|
||||
return;
|
||||
Dialogue& dialogue = play.character->dialogue;
|
||||
if (!isFinished)
|
||||
if (!isStarted)
|
||||
{
|
||||
if (!isStarted)
|
||||
isStarted = true;
|
||||
for (Effect& effect : entry->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)
|
||||
play.audio->play(effect.sound);
|
||||
}
|
||||
if (!isFinished && ++tick >= delay)
|
||||
{
|
||||
tick = 0;
|
||||
index++;
|
||||
delay = delayBase;
|
||||
effects_apply(play, index);
|
||||
auto pause = dialogue.pauses.find(letter_get(entry->text, index - 1));
|
||||
Pause found = pause == dialogue.pauses.end() ? Pause{} : pause->second;
|
||||
bool isSilentEffect = is_silent(*entry, index - 1);
|
||||
delay += found.delay;
|
||||
play.body.isTalking = !found.isSilent && !isSilentEffect;
|
||||
bool isBlipStep = dialogue.blipDelay > 0 && index % dialogue.blipDelay == 0;
|
||||
if (isSilentEffect)
|
||||
isBlipQueued = false;
|
||||
else if (found.isSilent)
|
||||
isBlipQueued |= isBlipStep;
|
||||
else if (isBlipStep || isBlipQueued)
|
||||
{
|
||||
isStarted = true;
|
||||
for (Effect& effect : entry->effects)
|
||||
if (effect.start == 0 && (effect.type == EFFECT_SCREENSHAKE || effect.type == EFFECT_SOUND))
|
||||
effect_apply(play, *this, effect);
|
||||
play.audio->play(dialogue.blip);
|
||||
isBlipQueued = false;
|
||||
}
|
||||
if (++tick >= delay)
|
||||
if (index >= entry->length)
|
||||
{
|
||||
tick = 0;
|
||||
index++;
|
||||
delay = delayBase;
|
||||
effects_apply(play, index);
|
||||
const char* letter = entry->text.c_str();
|
||||
const char* end = letter + entry->text.size();
|
||||
unsigned int codepoint = 0;
|
||||
for (int i = 0; i < index - 1 && letter < end; i++)
|
||||
letter += ImTextCharFromUtf8(&codepoint, letter, end);
|
||||
int letterSize = letter < end ? ImTextCharFromUtf8(&codepoint, letter, end) : 0;
|
||||
auto pause = dialogue.pauses.find(std::string(letter, letterSize));
|
||||
Pause found = pause == dialogue.pauses.end() ? Pause{} : pause->second;
|
||||
bool isSilentEffect = is_silent(*entry, index - 1);
|
||||
delay += found.delay;
|
||||
play.isTalking = !found.isSilent && !isSilentEffect;
|
||||
bool isBlipStep = dialogue.blipDelay > 0 && index % dialogue.blipDelay == 0;
|
||||
if (isSilentEffect)
|
||||
isBlipQueued = false;
|
||||
else if (found.isSilent)
|
||||
isBlipQueued |= isBlipStep;
|
||||
else if (isBlipStep || isBlipQueued)
|
||||
{
|
||||
play.audio->play(dialogue.blip);
|
||||
isBlipQueued = false;
|
||||
}
|
||||
if (index >= entry->length)
|
||||
{
|
||||
isFinished = true;
|
||||
isBlipQueued = false;
|
||||
play.isTalking = false;
|
||||
isScene &= !entry->isTerminal;
|
||||
}
|
||||
isFinished = true;
|
||||
isBlipQueued = false;
|
||||
play.body.isTalking = false;
|
||||
isScene &= !entry->isTerminal;
|
||||
}
|
||||
}
|
||||
|
||||
if (entry->text.empty())
|
||||
{
|
||||
Anm2Animation* animation = play.actor.animation_get();
|
||||
bool isReady = entry->animation.empty() || !animation || !play.actor.isPlaying ||
|
||||
Actor& actor = play.body.actor;
|
||||
Anm2Animation* animation = actor.animation_get();
|
||||
bool isReady = entry->animation.empty() || !animation || !actor.isPlaying ||
|
||||
(!isAnimationStarted && animation->length <= 1) ||
|
||||
(animation->isLoop && isAnimationStarted && play.actor.time < animationTimePrevious);
|
||||
(animation->isLoop && isAnimationStarted && actor.time < animationTimePrevious);
|
||||
isAnimationStarted = true;
|
||||
animationTimePrevious = play.actor.time;
|
||||
animationTimePrevious = actor.time;
|
||||
if (entry->next.empty())
|
||||
clear(play);
|
||||
else if (isReady)
|
||||
set(play, dialogue.entry_get(entry->next), TEXT_SEQUENCE);
|
||||
return;
|
||||
}
|
||||
|
||||
ImGuiStyle& style = ImGui::GetStyle();
|
||||
ImVec2 size = {screenSize.x * WIDTH_MULTIPLIER - style.WindowPadding.x * 2,
|
||||
ImGui::GetTextLineHeightWithSpacing() * HEIGHT_LINES};
|
||||
ImGui::SetNextWindowSize(size);
|
||||
ImGui::SetNextWindowPos({screenSize.x / 2 - size.x / 2, screenSize.y - size.y - style.WindowPadding.y});
|
||||
if (ImGui::Begin("##Text", nullptr,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove))
|
||||
{
|
||||
bool isHovered = ImGui::IsWindowHovered();
|
||||
bool isSpace = ImGui::IsKeyReleased(ImGuiKey_Space);
|
||||
bool isAdvance = (isHovered && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) || isSpace;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, {style.ItemSpacing.x, 0});
|
||||
if (ImGui::BeginTabBar("##Name"))
|
||||
{
|
||||
if (ImGui::BeginTabItem(play.character->name.c_str()))
|
||||
ImGui::EndTabItem();
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
ImGui::PushFont(nullptr, FONT_NORMAL);
|
||||
letters_draw(*entry, index, (float)ImGui::GetTime());
|
||||
|
||||
ImVec2 windowPosition = ImGui::GetWindowPos();
|
||||
ImVec2 contentMin = {windowPosition.x + style.WindowPadding.x, windowPosition.y + style.WindowPadding.y};
|
||||
ImVec2 contentMax = {windowPosition.x + ImGui::GetWindowWidth() - style.WindowPadding.x,
|
||||
windowPosition.y + ImGui::GetWindowHeight() - style.WindowPadding.y};
|
||||
Entry* current = entry;
|
||||
if (!isFinished && isAdvance)
|
||||
{
|
||||
for (int skipped = index + 1; skipped <= entry->length; skipped++)
|
||||
effects_apply(play, skipped);
|
||||
index = entry->length;
|
||||
isFinished = true;
|
||||
play.isTalking = false;
|
||||
isScene &= !entry->isTerminal;
|
||||
}
|
||||
else if (isFinished && !current->choices.empty())
|
||||
{
|
||||
float buttonY = contentMax.y - ImGui::GetFrameHeight();
|
||||
float separatorY = buttonY - style.ItemSpacing.y;
|
||||
ImGui::GetWindowDrawList()->AddLine({contentMin.x, separatorY}, {contentMax.x, separatorY},
|
||||
ImGui::GetColorU32(ImGuiCol_Separator));
|
||||
ImGui::SetCursorScreenPos({contentMin.x, buttonY});
|
||||
ImVec2 buttonSize = {row_width_get((int)current->choices.size()), 0};
|
||||
for (int i = 0; i < (int)current->choices.size(); i++)
|
||||
{
|
||||
Choice& choice = current->choices[i];
|
||||
ImGui::PushID(i);
|
||||
if (is_fx(ImGui::Button(choice.text.c_str(), buttonSize)))
|
||||
set(play, dialogue.entry_get(choice.next), TEXT_SEQUENCE);
|
||||
ImGui::SetItemTooltip("%s", choice.text.c_str());
|
||||
ImGui::PopID();
|
||||
if (i + 1 < (int)current->choices.size())
|
||||
ImGui::SameLine();
|
||||
}
|
||||
if (isHovered && isSpace)
|
||||
{
|
||||
play.audio->play(play.character->sounds[SOUND_SELECT]);
|
||||
set(play, dialogue.entry_get(current->choices.front().next), TEXT_SEQUENCE);
|
||||
}
|
||||
}
|
||||
else if (isFinished && !current->next.empty())
|
||||
{
|
||||
float side = ImGui::GetTextLineHeightWithSpacing();
|
||||
ImVec2 center = {contentMax.x - side / 2 +
|
||||
std::sin((float)ImGui::GetTime() * INDICATOR_SPEED) * side * INDICATOR_BOB,
|
||||
contentMax.y - side / 2};
|
||||
float half = side * INDICATOR_SIZE;
|
||||
ImGui::GetWindowDrawList()->AddTriangleFilled({center.x + half, center.y},
|
||||
{center.x - half, center.y - half},
|
||||
{center.x - half, center.y + half},
|
||||
ImGui::GetColorU32(ImGuiCol_Text));
|
||||
if (isAdvance)
|
||||
{
|
||||
play.audio->play(play.character->sounds[SOUND_SELECT]);
|
||||
set(play, dialogue.entry_get(current->next), TEXT_SEQUENCE);
|
||||
}
|
||||
}
|
||||
else if (isFinished && isAdvance)
|
||||
clear(play);
|
||||
ImGui::PopFont();
|
||||
ImGui::PopStyleVar();
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
if (entry && isFinished && entry->isTerminal && (life += ImGui::GetIO().DeltaTime) > LIFE_MAX)
|
||||
else if (isFinished && entry->isTerminal && (life += TICK) > LIFE_MAX)
|
||||
clear(play);
|
||||
}
|
||||
+4
-2
@@ -12,7 +12,7 @@ enum TextMode
|
||||
};
|
||||
|
||||
// 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.
|
||||
// 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:
|
||||
@@ -31,7 +31,9 @@ public:
|
||||
bool isStarted = false;
|
||||
|
||||
void set(Play& play, Entry* next, TextMode mode = TEXT_INTERRUPTIBLE, const std::string& onEnd = "");
|
||||
void update(Play& play, Vec2 screenSize);
|
||||
void update(Play& play);
|
||||
void advance(Play& play);
|
||||
void choose(Play& play, int choice);
|
||||
void clear(Play& play);
|
||||
bool is_interruptible();
|
||||
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
#include "play.h"
|
||||
|
||||
constexpr float AUTO_ACCELERATION = 0.45f;
|
||||
constexpr float AUTO_ANGLE_STEP = 0.08f;
|
||||
constexpr float AUTO_BRAKING = 2;
|
||||
constexpr float AUTO_CALORIE_EPSILON = 0.001f;
|
||||
constexpr float AUTO_DIGEST_ACCELERATION = 0.15f;
|
||||
constexpr float AUTO_DIGEST_RADIUS = 0.25f;
|
||||
constexpr float AUTO_DIGEST_SPEED = 3;
|
||||
constexpr float AUTO_SCALE_AREA = 512.0f * 512.0f;
|
||||
constexpr float AUTO_SCALE_MAX = 2;
|
||||
constexpr float AUTO_SCALE_MIN = 0.5f;
|
||||
constexpr float AUTO_SPEED = 9;
|
||||
constexpr float AUTO_TARGET_EPSILON = 0.001f;
|
||||
constexpr int AUTO_CLICK_COOLDOWN = 45;
|
||||
|
||||
void Interactions::update(Play& play, Pointer pointer)
|
||||
{
|
||||
Character& character = *play.character;
|
||||
Body& body = play.body;
|
||||
Text& text = play.text;
|
||||
Actor& active = play.pointer_actor_get();
|
||||
if (text.isScene)
|
||||
{
|
||||
isInteracting = isHolding = isHovering = false;
|
||||
area = -1;
|
||||
return;
|
||||
}
|
||||
bool wasInteracting = isInteracting;
|
||||
bool wasHolding = isHolding;
|
||||
bool wasHovering = isHovering;
|
||||
isHovering = false;
|
||||
isHolding &= isInteracting;
|
||||
cooldowns.resize(character.interactAreas.size());
|
||||
for (int& cooldown : cooldowns)
|
||||
cooldown = text.entry ? std::max(0, cooldown - 1) : 0;
|
||||
|
||||
for (int i = 0; i < (int)character.interactAreas.size(); i++)
|
||||
{
|
||||
InteractArea& entry = character.interactAreas[i];
|
||||
bool isInside = is_point_in_rect(body.actor.null_rect_get(entry.null), pointer.position);
|
||||
if (play.cursorState == CURSOR_DEFAULT && isInside && !pointer.isCaptured && entry.type == pointer.type)
|
||||
{
|
||||
play.cursorState = CURSOR_HOVER;
|
||||
active.play(entry.cursorHover, TRANSITION_IF_IDLE);
|
||||
isHovering = true;
|
||||
area = i;
|
||||
bool isHoldStart = entry.isHold && (pointer.isClicked || (pointer.isDown && !isInteracting));
|
||||
if (isHoldStart || (!entry.isHold && pointer.isClicked))
|
||||
{
|
||||
if (entry.isHold)
|
||||
isInteracting = isHolding = true;
|
||||
else
|
||||
{
|
||||
play.cursorState = CURSOR_ACTION;
|
||||
active.play(entry.cursorActive, TRANSITION_FORCE, INTERRUPT_ALWAYS);
|
||||
}
|
||||
play.audio->play(entry.sounds);
|
||||
body.idle_override_set(entry.idleOnEnd);
|
||||
play.particles_spawn(entry.particles, pointer.position);
|
||||
if (entry.digestionClick > 0 && body.stats.calories > 0 && !body.stats.isDigesting)
|
||||
body.stats.digestionProgress += entry.digestionClick;
|
||||
body.actor.pulse_add(entry.layer, entry.scaleAmplitude, entry.scaleCycles, entry.scaleTime);
|
||||
bool isReady = cooldowns[i] <= 0;
|
||||
cooldowns[i] = std::max(0, entry.dialogueCooldown);
|
||||
Dialogue& dialogue = character.dialogue;
|
||||
bool isFull = body.is_over_capacity() && dialogue.pools.contains(entry.dialoguePoolFull);
|
||||
const std::string& pool = isFull ? entry.dialoguePoolFull : entry.dialoguePool;
|
||||
if (dialogue.pools.contains(pool))
|
||||
{
|
||||
if (text.is_interruptible() && isReady)
|
||||
text.set(play, dialogue.pool_get(pool), TEXT_INTERRUPTIBLE, entry.isHold ? "" : entry.idleOnEnd);
|
||||
}
|
||||
else if (entry.dialoguePool.empty() && !entry.animation.empty())
|
||||
body.play(entry.animation, TRANSITION_FORCE);
|
||||
}
|
||||
if (isInteracting)
|
||||
{
|
||||
play.cursorState = CURSOR_ACTION;
|
||||
active.play(entry.cursorActive);
|
||||
Vec2 moved = pointer.position - pointerPrevious;
|
||||
if (entry.digestionHover > 0 && body.stats.calories > 0 && !body.stats.isDigesting &&
|
||||
(moved.x != 0 || moved.y != 0))
|
||||
body.stats.digestionProgress += entry.digestionHover;
|
||||
}
|
||||
}
|
||||
if ((i == area && !isInside) || pointer.isReleased || pointer.isCaptured)
|
||||
{
|
||||
isInteracting = isHolding = false;
|
||||
area = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if ((wasInteracting && !isInteracting) || (wasHovering && !isHovering))
|
||||
active.play(character.cursor.idle);
|
||||
if (wasHolding && !isHolding && !body.actor.pending)
|
||||
body.idle_play();
|
||||
pointerPrevious = pointer.position;
|
||||
}
|
||||
|
||||
// Moves the cursor toward a target, braking to land on it. Returns whether it's there.
|
||||
static bool is_target_reached(Vec2& position, Vec2& velocity, Vec2 target, float speed, float acceleration)
|
||||
{
|
||||
Vec2 delta = target - position;
|
||||
float distance = vec2_length(delta);
|
||||
if (!std::isfinite(distance))
|
||||
return false;
|
||||
if (distance <= AUTO_TARGET_EPSILON)
|
||||
{
|
||||
position = target;
|
||||
velocity = {};
|
||||
return true;
|
||||
}
|
||||
Vec2 desired = delta / distance * std::min(speed, std::sqrt(AUTO_BRAKING * acceleration * distance));
|
||||
Vec2 change = desired - velocity;
|
||||
float changeLength = vec2_length(change);
|
||||
velocity = changeLength > acceleration ? velocity + change / changeLength * acceleration : desired;
|
||||
if (vec2_length(velocity) >= distance)
|
||||
{
|
||||
position = target;
|
||||
velocity = {};
|
||||
return true;
|
||||
}
|
||||
position = position + velocity;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_autofeed_food(const Item& item)
|
||||
{
|
||||
return item.use == USE_EDIBLE && item.capacityBonus >= 0 && item.digestionBonus >= 0;
|
||||
}
|
||||
|
||||
static bool is_edible(Play& play, const Item& item)
|
||||
{
|
||||
return is_autofeed_food(item) && item.calories > 0 &&
|
||||
play.body.stats.calories + item.calories <= play.body.capacity_max_get();
|
||||
}
|
||||
|
||||
// The edible item in the world with the most calories, ties broken at random.
|
||||
static int food_best_get(Play& play)
|
||||
{
|
||||
std::vector<Item>& catalog = play.character->catalog.items;
|
||||
std::vector<ItemEntity>& list = play.items.list;
|
||||
int best = -1;
|
||||
int ties = 0;
|
||||
for (int i = 0; i < (int)list.size(); i++)
|
||||
{
|
||||
Item& item = catalog[list[i].id];
|
||||
if (!is_edible(play, item))
|
||||
continue;
|
||||
float bestCalories = best >= 0 ? catalog[list[best].id].calories : -1;
|
||||
if (item.calories > bestCalories)
|
||||
best = i, ties = 1;
|
||||
else if (item.calories == bestCalories && SDL_rand(++ties) == 0)
|
||||
best = i;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// The first interact area that helps digestion, which the autofeed rubs or pats.
|
||||
static InteractArea* digest_area_get(Play& play)
|
||||
{
|
||||
for (InteractArea& area : play.character->interactAreas)
|
||||
if (area.digestionClick > 0 || area.digestionHover > 0)
|
||||
return &area;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int Autofeed::type_get(Play& play)
|
||||
{
|
||||
InteractArea* area = digest_area_get(play);
|
||||
return area ? area->type : play.cursorType;
|
||||
}
|
||||
|
||||
bool Autofeed::is_available(Play& play)
|
||||
{
|
||||
bool isFood = play.body.stats.calories > 0 || play.items.is_spawn_possible(play);
|
||||
for (ItemEntity& entity : play.items.list)
|
||||
isFood |= play.character->catalog.items[entity.id].use == USE_EDIBLE;
|
||||
return isFood;
|
||||
}
|
||||
|
||||
void Autofeed::reset()
|
||||
{
|
||||
isItemHeld = isDigestHeld = false;
|
||||
target = -1;
|
||||
clickCooldown = 0;
|
||||
velocity = {};
|
||||
}
|
||||
|
||||
void Autofeed::update(Play& play, Pointer& itemPointer, Pointer& interactPointer)
|
||||
{
|
||||
Body& body = play.body;
|
||||
Items& items = play.items;
|
||||
Actor& cursor = play.autoCursor;
|
||||
std::vector<Item>& catalog = play.character->catalog.items;
|
||||
cursor.isVisible = false;
|
||||
bool isWorldFood = false;
|
||||
bool isAnyEdible = false;
|
||||
for (ItemEntity& entity : items.list)
|
||||
{
|
||||
isWorldFood |= is_autofeed_food(catalog[entity.id]);
|
||||
isAnyEdible |= is_edible(play, catalog[entity.id]);
|
||||
}
|
||||
if (!isEnabled || body.isStageUp)
|
||||
return reset();
|
||||
items.spawn_queue_add(play);
|
||||
if (body.stats.calories <= AUTO_CALORIE_EPSILON && !isWorldFood)
|
||||
return reset();
|
||||
|
||||
if (cursor.position.x == 0 && cursor.position.y == 0)
|
||||
cursor.position = body.actor.position;
|
||||
Rect bodyBounds = body.actor.bounds_get();
|
||||
float speedScale = is_rect_valid(bodyBounds) ? std::clamp(std::sqrt(bodyBounds.w * bodyBounds.h / AUTO_SCALE_AREA),
|
||||
AUTO_SCALE_MIN, AUTO_SCALE_MAX)
|
||||
: 1;
|
||||
Rect mouth = {NAN, NAN, NAN, NAN};
|
||||
for (EatArea& eatArea : play.character->eatAreas)
|
||||
if (!is_rect_valid(mouth))
|
||||
mouth = body.eat_rect_get(eatArea);
|
||||
bool isMouth = is_rect_valid(mouth);
|
||||
auto move = [&](Vec2 destination, float speed, float acceleration) {
|
||||
isActive = cursor.isVisible = true;
|
||||
itemPointer = interactPointer = {.position = cursor.position, .isAutomated = true};
|
||||
bool isThere =
|
||||
is_target_reached(cursor.position, velocity, destination, speed * speedScale, acceleration * speedScale);
|
||||
itemPointer.position = interactPointer.position = cursor.position;
|
||||
return isThere;
|
||||
};
|
||||
|
||||
if (items.held >= 0 && items.held < (int)items.list.size())
|
||||
{
|
||||
bool isCarried = isMouth && is_edible(play, catalog[items.list[items.held].id]);
|
||||
move(isCarried ? rect_middle(mouth) : cursor.position, AUTO_SPEED, AUTO_ACCELERATION);
|
||||
itemPointer.isDown = isItemHeld = isCarried;
|
||||
itemPointer.isReleased = !isCarried;
|
||||
isDigestHeld = false;
|
||||
target = isCarried ? target : -1;
|
||||
return;
|
||||
}
|
||||
|
||||
bool isTargetValid = target >= 0 && target < (int)items.list.size() && is_edible(play, catalog[items.list[target].id]);
|
||||
if (!isMouth || !isTargetValid)
|
||||
target = isMouth ? food_best_get(play) : -1;
|
||||
if (target >= 0)
|
||||
{
|
||||
Rect rect = items.list[target].actor.bounds_get();
|
||||
if (!is_rect_valid(rect))
|
||||
return reset();
|
||||
move(rect_middle(rect), AUTO_SPEED, AUTO_ACCELERATION);
|
||||
bool isInside = is_point_in_rect(rect, cursor.position);
|
||||
itemPointer.target = target;
|
||||
itemPointer.isClicked = isInside && !isItemHeld;
|
||||
itemPointer.isDown = isInside || isItemHeld;
|
||||
isItemHeld |= isInside;
|
||||
isDigestHeld = false;
|
||||
return;
|
||||
}
|
||||
if (!isWorldFood && isAnyEdible)
|
||||
return reset();
|
||||
|
||||
InteractArea* area = digest_area_get(play);
|
||||
Rect rect = area ? body.actor.null_rect_get(area->null) : Rect{NAN, NAN, NAN, NAN};
|
||||
if (!is_rect_valid(rect))
|
||||
return reset();
|
||||
angle += AUTO_ANGLE_STEP;
|
||||
float radius = std::max(1.0f, std::min(rect.w, rect.h) * AUTO_DIGEST_RADIUS);
|
||||
Vec2 destination = rect_middle(rect) + Vec2{std::cos(angle), std::sin(angle)} * radius;
|
||||
bool isThere = move(destination, AUTO_DIGEST_SPEED, AUTO_DIGEST_ACCELERATION);
|
||||
interactPointer.type = area->type;
|
||||
if (area->digestionHover > 0)
|
||||
{
|
||||
bool isInside = is_point_in_rect(rect, cursor.position);
|
||||
interactPointer.isClicked = isInside && !isDigestHeld;
|
||||
interactPointer.isDown = isInside;
|
||||
interactPointer.isReleased = isDigestHeld && !isInside;
|
||||
isDigestHeld = isInside;
|
||||
}
|
||||
else
|
||||
{
|
||||
clickCooldown = std::max(0, clickCooldown - 1);
|
||||
interactPointer.isClicked = isThere && clickCooldown <= 0 && !isDigestHeld;
|
||||
interactPointer.isReleased = isDigestHeld;
|
||||
if (interactPointer.isClicked)
|
||||
clickCooldown = AUTO_CLICK_COOLDOWN;
|
||||
isDigestHeld = interactPointer.isClicked;
|
||||
}
|
||||
isItemHeld = false;
|
||||
}
|
||||
+166
-442
@@ -1,139 +1,123 @@
|
||||
#include "play.h"
|
||||
#include <imgui.h>
|
||||
|
||||
constexpr float AUTO_ACCELERATION = 0.45f;
|
||||
constexpr float AUTO_ANGLE_STEP = 0.08f;
|
||||
constexpr float AUTO_BRAKING = 2;
|
||||
constexpr float AUTO_CALORIE_EPSILON = 0.001f;
|
||||
constexpr float AUTO_DIGEST_ACCELERATION = 0.15f;
|
||||
constexpr float AUTO_DIGEST_RADIUS = 0.25f;
|
||||
constexpr float AUTO_DIGEST_SPEED = 3;
|
||||
constexpr float AUTO_SCALE_MAX = 2;
|
||||
constexpr float AUTO_SCALE_MIN = 0.5f;
|
||||
constexpr float AUTO_SCALE_AREA = 512.0f * 512.0f;
|
||||
constexpr float AUTO_SPEED = 9;
|
||||
constexpr float AUTO_TARGET_EPSILON = 0.001f;
|
||||
constexpr int AUTO_CLICK_COOLDOWN = 45;
|
||||
constexpr float HOLD_SPIN = 0.1f;
|
||||
constexpr float ROTATION_GROUND_DAMPING = 0.85f;
|
||||
constexpr float ROTATION_MAX = 90;
|
||||
constexpr float ROTATION_RETURN_SPEED = 120;
|
||||
constexpr float THROW_THRESHOLD = 10;
|
||||
|
||||
void Play::item_add(int id, Vec2 position, Vec2 velocity, float rotation)
|
||||
void Items::add(Play& play, int id, Vec2 position, Vec2 velocity, float rotation)
|
||||
{
|
||||
ItemEntity& entity = items.emplace_back(ItemEntity{character->item_actor_get(id), velocity, 0, id});
|
||||
ItemEntity& entity = list.emplace_back(ItemEntity{play.character->item_actor_get(id), velocity, 0, id});
|
||||
entity.actor.position = position;
|
||||
entity.actor.overrides.push_back({.rotation = rotation, .target = TARGET_ROOT, .isRotation = true});
|
||||
entity.actor.rotation = rotation;
|
||||
discovered[id] = true;
|
||||
}
|
||||
|
||||
int Play::spawn_delay_get()
|
||||
// Forgets which item is being eaten, and lets go of the held one unless asked to keep it.
|
||||
void Items::drop(Hold hold)
|
||||
{
|
||||
return std::max(1, (int)std::round(character->spawnDelay * std::pow(character->spawnDelayMultiplier, stage)));
|
||||
if (hold == HOLD_RELEASE)
|
||||
{
|
||||
held = -1;
|
||||
feeding.animation = -1;
|
||||
}
|
||||
feeding.item = -1;
|
||||
feeding.event = -1;
|
||||
feeding.rect = {NAN, NAN, NAN, NAN};
|
||||
}
|
||||
|
||||
int Items::spawn_delay_get(Play& play)
|
||||
{
|
||||
Catalog& catalog = play.character->catalog;
|
||||
return std::max(1, (int)std::round(catalog.spawnDelay * std::pow(catalog.spawnDelayMultiplier, play.body.stage)));
|
||||
}
|
||||
|
||||
static float spawn_weight_get(Play& play, int id)
|
||||
{
|
||||
Item& item = play.character->items[id];
|
||||
if (item.calories > play.capacity_max_get() || item.rarity < 0)
|
||||
Catalog& catalog = play.character->catalog;
|
||||
Item& item = catalog.items[id];
|
||||
if (item.calories > play.body.capacity_max_get() || item.rarity < 0)
|
||||
return 0;
|
||||
return std::max(0.0f, play.character->rarities[item.rarity].weight);
|
||||
return std::max(0.0f, catalog.rarities[item.rarity].weight);
|
||||
}
|
||||
|
||||
bool Play::is_spawn_possible()
|
||||
bool Items::is_spawn_possible(Play& play)
|
||||
{
|
||||
if ((int)items.size() >= character->spawnLimit)
|
||||
Catalog& catalog = play.character->catalog;
|
||||
if ((int)list.size() >= catalog.spawnLimit)
|
||||
return false;
|
||||
for (int i = 0; i < (int)character->items.size(); i++)
|
||||
if (spawn_weight_get(*this, i) > 0)
|
||||
for (int i = 0; i < (int)catalog.items.size(); i++)
|
||||
if (spawn_weight_get(play, i) > 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void Play::spawn_queue_add(int id)
|
||||
void Items::spawn_queue_add(Play& play, int id)
|
||||
{
|
||||
if (id < 0 || id >= (int)character->items.size())
|
||||
Catalog& catalog = play.character->catalog;
|
||||
if (id < 0 || id >= (int)catalog.items.size())
|
||||
return;
|
||||
Item& item = character->items[id];
|
||||
audio->play(character->sounds[SOUND_SUMMON]);
|
||||
if (item.rarity >= 0)
|
||||
audio->play(character->rarities[item.rarity].sound);
|
||||
play.audio->play(play.character->sounds[SOUND_SUMMON]);
|
||||
if (catalog.items[id].rarity >= 0)
|
||||
play.audio->play(catalog.rarities[catalog.items[id].rarity].sound);
|
||||
spawnQueue.push_back(id);
|
||||
spawnDelay = spawn_delay_get();
|
||||
spawnDelay = spawn_delay_get(play);
|
||||
}
|
||||
|
||||
// Queues an item picked at random by rarity, once the spawn delay is up.
|
||||
void Play::spawn_queue_add()
|
||||
void Items::spawn_queue_add(Play& play)
|
||||
{
|
||||
if (spawnDelay > 0 || !is_spawn_possible())
|
||||
if (spawnDelay > 0 || !is_spawn_possible(play))
|
||||
return;
|
||||
std::vector<Item>& items = play.character->catalog.items;
|
||||
float total = 0;
|
||||
for (int i = 0; i < (int)character->items.size(); i++)
|
||||
total += spawn_weight_get(*this, i);
|
||||
for (int i = 0; i < (int)items.size(); i++)
|
||||
total += spawn_weight_get(play, i);
|
||||
float roll = random_get(0, total);
|
||||
for (int i = 0; i < (int)character->items.size(); i++)
|
||||
if (float weight = spawn_weight_get(*this, i); weight > 0 && (roll -= weight) < 0)
|
||||
return spawn_queue_add(i);
|
||||
for (int i = 0; i < (int)items.size(); i++)
|
||||
if (float weight = spawn_weight_get(play, i); weight > 0 && (roll -= weight) < 0)
|
||||
return spawn_queue_add(play, i);
|
||||
}
|
||||
|
||||
// Where an eat area takes food: its null now, or at the start of its animation when it isn't showing.
|
||||
Rect Play::eat_rect_get(const EatArea& eatArea)
|
||||
void Items::update(Play& play, Pointer pointer)
|
||||
{
|
||||
Rect rect = actor.null_rect_get(eatArea.null);
|
||||
if (!is_rect_valid(rect))
|
||||
rect = actor.null_rect_get(eatArea.null, actor.anm2->animation_get(eatArea.animation + suffix_get()), 0);
|
||||
return rect;
|
||||
}
|
||||
|
||||
// Forgets which item is being eaten, and lets go of the held one when asked.
|
||||
void Play::item_drop(Hold hold)
|
||||
{
|
||||
heldItem = hold == HOLD_RELEASE ? -1 : heldItem;
|
||||
eatAnimation = hold == HOLD_RELEASE ? -1 : eatAnimation;
|
||||
eatItem = -1;
|
||||
eatEvent = -1;
|
||||
eatRect = {NAN, NAN, NAN, NAN};
|
||||
}
|
||||
|
||||
static Actor& pointer_actor_get(Play& play)
|
||||
{
|
||||
return play.isAutofeedActive ? play.autoCursor : play.cursor;
|
||||
}
|
||||
|
||||
void Play::items_update(Pointer pointer)
|
||||
{
|
||||
Character& data = *character;
|
||||
Actor& active = pointer_actor_get(*this);
|
||||
int areaIndex = data.area_get(stage);
|
||||
Area area = areaIndex >= 0 ? data.areas[areaIndex] : Area{};
|
||||
bool isOver = is_over_capacity();
|
||||
auto text_play = [&](const char* hook) {
|
||||
Character& character = *play.character;
|
||||
Body& body = play.body;
|
||||
Stats& stats = body.stats;
|
||||
Actor& active = play.pointer_actor_get();
|
||||
Audio& audio = *play.audio;
|
||||
int areaIndex = character.area_get(body.stage);
|
||||
Area area = areaIndex >= 0 ? character.areas[areaIndex] : Area{};
|
||||
Rect bounds = play.bounds;
|
||||
bool isOver = body.is_over_capacity();
|
||||
auto text_play = [&](Hook hook) {
|
||||
if (!pointer.isAutomated)
|
||||
text.set(*this, data.dialogue.hook_get(hook));
|
||||
play.text.set(play, character.dialogue.hook_get(hook));
|
||||
};
|
||||
auto held_release = [&](bool isThrown) {
|
||||
item_drop();
|
||||
active.play(data.cursorIdle);
|
||||
if (!isThrown && !actor.pending)
|
||||
idle_play();
|
||||
auto release = [&](bool isThrown) {
|
||||
drop();
|
||||
active.play(character.cursor.idle);
|
||||
if (!isThrown && !body.actor.pending)
|
||||
body.idle_play();
|
||||
};
|
||||
if (text.isScene && heldItem >= 0)
|
||||
if (play.text.isScene && held >= 0)
|
||||
{
|
||||
held_release(false);
|
||||
isEatTextPending = false;
|
||||
release(false);
|
||||
feeding.isTextPending = false;
|
||||
}
|
||||
|
||||
if (isEatTextPending && eatAnimation == actor.animation && (!actor.isPlaying || actor.is_finished()))
|
||||
if (feeding.isTextPending && feeding.animation == body.actor.animation &&
|
||||
(!body.actor.isPlaying || body.actor.is_finished()))
|
||||
{
|
||||
text.set(*this, data.dialogue.hook_get(isEatTextFull ? "EatFull" : "Eat"));
|
||||
isEatTextPending = false;
|
||||
eatAnimation = -1;
|
||||
play.text.set(play, character.dialogue.hook_get(feeding.isTextFull ? HOOK_EAT_FULL : HOOK_EAT));
|
||||
feeding.isTextPending = false;
|
||||
feeding.animation = -1;
|
||||
}
|
||||
|
||||
bool wasItemHovered = isItemHovered;
|
||||
isItemHovered = false;
|
||||
tooltipItem = -1;
|
||||
bool wasHovered = isHovered;
|
||||
isHovered = false;
|
||||
tooltip = -1;
|
||||
if (spawnDelay > 0)
|
||||
spawnDelay--;
|
||||
for (int id : spawnQueue)
|
||||
@@ -142,149 +126,151 @@ void Play::items_update(Pointer pointer)
|
||||
Vec2 position = {spawn.x + random_get(0, spawn.w), spawn.y + random_get(0, spawn.h)};
|
||||
if (!discovered[id])
|
||||
{
|
||||
toast_add(STRING_ToastNewItemFound);
|
||||
audio->play(data.sounds[SOUND_NEW_ITEM]);
|
||||
play.toast_add("New item found!");
|
||||
audio.play(character.sounds[SOUND_NEW_ITEM]);
|
||||
}
|
||||
item_add(id, position);
|
||||
progress.totalSpawned++;
|
||||
particles_spawn(data.summonParticles, position);
|
||||
add(play, id, position);
|
||||
stats.totalSpawned++;
|
||||
play.particles_spawn(character.catalog.summonParticles, position);
|
||||
}
|
||||
spawnQueue.clear();
|
||||
|
||||
if (heldItem >= 0 && heldItem < (int)items.size())
|
||||
if (held >= 0 && held < (int)list.size())
|
||||
{
|
||||
ItemEntity& held = items[heldItem];
|
||||
Item& item = data.items[held.id];
|
||||
float& rotation = held.rotation_get();
|
||||
Vec2 delta = itemPointerPrevious - pointer.position;
|
||||
held.actor.position = pointer.position;
|
||||
held.velocity = {};
|
||||
held.angularVelocity = (held.angularVelocity - delta.x * HOLD_SPIN) * area.friction;
|
||||
ItemEntity& entity = list[held];
|
||||
Item& item = character.catalog.items[entity.id];
|
||||
float& rotation = *entity.actor.rotation;
|
||||
Vec2 delta = pointerPrevious - pointer.position;
|
||||
entity.actor.position = pointer.position;
|
||||
entity.velocity = {};
|
||||
entity.angularVelocity = (entity.angularVelocity - delta.x * HOLD_SPIN) * area.friction;
|
||||
rotation = std::clamp(rotation * area.friction, -ROTATION_MAX, ROTATION_MAX);
|
||||
int removed = -1;
|
||||
|
||||
if (item.use == USE_EDIBLE)
|
||||
{
|
||||
bool isEdible = item.calories > 0 && progress.calories + item.calories <= capacity_max_get();
|
||||
if (isJustItemHeld)
|
||||
text_play(isEdible ? (isOver ? "FeedFull" : "Feed")
|
||||
: item.calories > progress.capacity ? "LowCapacity"
|
||||
: "Full");
|
||||
isJustItemHeld = false;
|
||||
for (EatArea& eatArea : data.eatAreas)
|
||||
bool isEdible = item.calories > 0 && stats.calories + item.calories <= body.capacity_max_get();
|
||||
if (isJustHeld)
|
||||
text_play(isEdible ? (isOver ? HOOK_FEED_FULL : HOOK_FEED)
|
||||
: item.calories > stats.capacity ? HOOK_LOW_CAPACITY
|
||||
: HOOK_FULL);
|
||||
isJustHeld = false;
|
||||
for (EatArea& eatArea : character.eatAreas)
|
||||
{
|
||||
Rect rect = eat_rect_get(eatArea);
|
||||
bool isInside = is_point_in_rect(rect, held.actor.position);
|
||||
Rect rect = body.eat_rect_get(eatArea);
|
||||
bool isInside = is_point_in_rect(rect, entity.actor.position);
|
||||
bool isAreaActive = isEdible && isInside;
|
||||
bool isPending = eatItem == heldItem && eatEvent == eatArea.event;
|
||||
bool isPendingActive = isPending && is_point_in_rect(eatRect, held.actor.position);
|
||||
std::string eatName = eatArea.animation + suffix_get();
|
||||
bool isPending = feeding.item == held && feeding.event == eatArea.event;
|
||||
bool isPendingActive = isPending && is_point_in_rect(feeding.rect, entity.actor.position);
|
||||
std::string eatName = eatArea.animation + body.suffix;
|
||||
if (isAreaActive || isPendingActive)
|
||||
{
|
||||
if (isPending && eatArea.event >= 0 && actor.event == eatArea.event)
|
||||
if (isPending && eatArea.event >= 0 && body.actor.event == eatArea.event)
|
||||
{
|
||||
actor.event = -1;
|
||||
progress.calories += item.calories;
|
||||
progress.totalCalories += item.calories;
|
||||
progress.capacity = std::clamp(progress.capacity + item.capacityBonus, data.capacityMin,
|
||||
data.capacityMax);
|
||||
progress.digestionRate = std::clamp(progress.digestionRate + item.digestionBonus,
|
||||
data.digestionRateMin, data.digestionRateMax);
|
||||
progress.totalConsumed++;
|
||||
removed = heldItem;
|
||||
item_drop();
|
||||
eatAnimation = actor.animation;
|
||||
isEatTextPending = !pointer.isAutomated;
|
||||
isEatTextFull = isOver;
|
||||
Traits& traits = character.traits;
|
||||
body.actor.event = -1;
|
||||
stats.calories += item.calories;
|
||||
stats.totalCalories += item.calories;
|
||||
stats.capacity =
|
||||
std::clamp(stats.capacity + item.capacityBonus, traits.capacityMin, traits.capacityMax);
|
||||
stats.digestionRate = std::clamp(stats.digestionRate + item.digestionBonus,
|
||||
traits.digestionRateMin, traits.digestionRateMax);
|
||||
stats.totalConsumed++;
|
||||
removed = held;
|
||||
drop();
|
||||
feeding.animation = body.actor.animation;
|
||||
feeding.isTextPending = !pointer.isAutomated;
|
||||
feeding.isTextFull = isOver;
|
||||
break;
|
||||
}
|
||||
eatItem = heldItem;
|
||||
eatEvent = eatArea.event;
|
||||
feeding.item = held;
|
||||
feeding.event = eatArea.event;
|
||||
if (isAreaActive)
|
||||
eatRect = rect;
|
||||
if (!actor.is_playing(eatName))
|
||||
body_play(eatArea.animation, TRANSITION_NORMAL, INTERRUPT_ALWAYS);
|
||||
feeding.rect = rect;
|
||||
if (!body.actor.is_playing(eatName))
|
||||
body.play(eatArea.animation, TRANSITION_NORMAL, INTERRUPT_ALWAYS);
|
||||
}
|
||||
else if (eatItem == heldItem)
|
||||
item_drop(HOLD_KEEP);
|
||||
if (actor.is_playing(eatName) && !isInside)
|
||||
text_play(isOver ? "FoodTakenFull" : "FoodTaken");
|
||||
else if (feeding.item == held)
|
||||
drop(HOLD_KEEP);
|
||||
if (body.actor.is_playing(eatName) && !isInside)
|
||||
text_play(isOver ? HOOK_FOOD_TAKEN_FULL : HOOK_FOOD_TAKEN);
|
||||
if (isAreaActive)
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (item.use == USE_TOOL && item.isToggleSpritesheet &&
|
||||
is_point_in_rect(actor.bounds_get(), held.actor.position) && data.alternateTexture &&
|
||||
data.alternateSheet < (int)actor.sheets.size())
|
||||
is_point_in_rect(body.actor.bounds_get(), entity.actor.position) && character.alternate.texture &&
|
||||
character.alternate.sheet < (int)body.actor.sheets.size())
|
||||
{
|
||||
progress.isAlternate = !progress.isAlternate;
|
||||
actor.sheets[data.alternateSheet] =
|
||||
progress.isAlternate ? data.alternateTexture : data.anm2->sheets[data.alternateSheet];
|
||||
audio->play(data.alternateSound);
|
||||
removed = heldItem;
|
||||
item_drop();
|
||||
stats.isAlternate = !stats.isAlternate;
|
||||
body.actor.sheets[character.alternate.sheet] =
|
||||
stats.isAlternate ? character.alternate.texture : character.anm2->sheets[character.alternate.sheet];
|
||||
audio.play(character.alternate.sound);
|
||||
removed = held;
|
||||
drop();
|
||||
}
|
||||
|
||||
if (removed < 0 && pointer.isReleased)
|
||||
{
|
||||
bool isThrown = std::abs(delta.x) >= THROW_THRESHOLD || std::abs(delta.y) >= THROW_THRESHOLD;
|
||||
audio->play(data.sounds[isThrown ? SOUND_THROW : SOUND_RELEASE]);
|
||||
audio.play(character.sounds[isThrown ? SOUND_THROW : SOUND_RELEASE]);
|
||||
if (isThrown)
|
||||
text_play("Throw");
|
||||
held.velocity = held.velocity - delta;
|
||||
held_release(isThrown);
|
||||
text_play(HOOK_THROW);
|
||||
entity.velocity = entity.velocity - delta;
|
||||
release(isThrown);
|
||||
}
|
||||
if (removed >= 0)
|
||||
items.erase(items.begin() + removed);
|
||||
list.erase(list.begin() + removed);
|
||||
}
|
||||
|
||||
int moved = -1;
|
||||
bool isTargetLocked = pointer.target >= 0 && pointer.target < (int)items.size();
|
||||
bool isTargetLocked = pointer.target >= 0 && pointer.target < (int)list.size();
|
||||
float right = bounds.x + bounds.w;
|
||||
float bottom = bounds.y + bounds.h;
|
||||
for (int i = 0; i < (int)items.size(); i++)
|
||||
for (int i = 0; i < (int)list.size(); i++)
|
||||
{
|
||||
ItemEntity& entity = items[i];
|
||||
ItemEntity& entity = list[i];
|
||||
Vec2& position = entity.actor.position;
|
||||
Vec2& velocity = entity.velocity;
|
||||
float& rotation = entity.rotation_get();
|
||||
float gravity = data.items[entity.id].gravity != 0 ? data.items[entity.id].gravity : area.gravity;
|
||||
entity.actor.update(*audio);
|
||||
float& rotation = *entity.actor.rotation;
|
||||
float gravity = character.catalog.items[entity.id].gravity;
|
||||
gravity = gravity != 0 ? gravity : area.gravity;
|
||||
entity.actor.update(audio);
|
||||
position = position + velocity;
|
||||
rotation += entity.angularVelocity;
|
||||
|
||||
if (is_point_in_rect(entity.actor.bounds_get(), pointer.position) && !pointer.isCaptured &&
|
||||
(!isTargetLocked || i == pointer.target))
|
||||
{
|
||||
isItemHovered = true;
|
||||
tooltipItem = heldItem == i || pointer.isAutomated ? -1 : entity.id;
|
||||
active.play(data.cursorHover, TRANSITION_IF_IDLE);
|
||||
cursorState = CURSOR_HOVER;
|
||||
isHovered = true;
|
||||
tooltip = held == i || pointer.isAutomated ? -1 : entity.id;
|
||||
active.play(character.cursor.hover, TRANSITION_IF_IDLE);
|
||||
play.cursorState = CURSOR_HOVER;
|
||||
if (pointer.isClicked)
|
||||
{
|
||||
audio->play(data.sounds[SOUND_GRAB]);
|
||||
particles_spawn(data.grabParticles, pointer.position);
|
||||
isJustItemHeld = true;
|
||||
audio.play(character.sounds[SOUND_GRAB]);
|
||||
play.particles_spawn(character.cursor.grabParticles, pointer.position);
|
||||
isJustHeld = true;
|
||||
}
|
||||
if (pointer.isDown)
|
||||
{
|
||||
active.play(data.cursorGrab);
|
||||
cursorState = CURSOR_ACTION;
|
||||
heldItem = moved = i;
|
||||
active.play(character.cursor.grab);
|
||||
play.cursorState = CURSOR_ACTION;
|
||||
held = moved = i;
|
||||
}
|
||||
if (pointer.isRightClicked)
|
||||
{
|
||||
audio->play(data.sounds[SOUND_DISPOSE]);
|
||||
active.play(data.cursorDispose, TRANSITION_FORCE, INTERRUPT_NEVER);
|
||||
particles_spawn(data.disposeParticles, position);
|
||||
heldItem = heldItem == i ? -1 : heldItem > i ? heldItem - 1 : heldItem;
|
||||
audio.play(character.sounds[SOUND_DISPOSE]);
|
||||
active.play(character.cursor.dispose, TRANSITION_FORCE, INTERRUPT_NEVER);
|
||||
play.particles_spawn(character.catalog.disposeParticles, position);
|
||||
held = held == i ? -1 : held > i ? held - 1 : held;
|
||||
moved = moved == i ? -1 : moved > i ? moved - 1 : moved;
|
||||
items.erase(items.begin() + i--);
|
||||
list.erase(list.begin() + i--);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (i != heldItem)
|
||||
if (i != held)
|
||||
{
|
||||
if (position.x <= bounds.x || position.x >= right)
|
||||
{
|
||||
@@ -292,7 +278,7 @@ void Play::items_update(Pointer pointer)
|
||||
velocity.x = -velocity.x * area.friction;
|
||||
entity.angularVelocity = -entity.angularVelocity * area.friction;
|
||||
rotation = -rotation;
|
||||
audio->play(data.sounds[SOUND_BOUNCE]);
|
||||
audio.play(character.sounds[SOUND_BOUNCE]);
|
||||
}
|
||||
if (position.y <= bounds.y || position.y >= bottom)
|
||||
{
|
||||
@@ -301,7 +287,7 @@ void Play::items_update(Pointer pointer)
|
||||
position.y = bottom;
|
||||
velocity = {velocity.x * area.friction, 0};
|
||||
rotation = std::remainder(rotation, 360.0f);
|
||||
float step = ROTATION_RETURN_SPEED * ImGui::GetIO().DeltaTime;
|
||||
float step = ROTATION_RETURN_SPEED * TICK;
|
||||
rotation = std::abs(rotation) <= step ? 0 : rotation - std::copysign(step, rotation);
|
||||
entity.angularVelocity *= ROTATION_GROUND_DAMPING;
|
||||
}
|
||||
@@ -309,7 +295,7 @@ void Play::items_update(Pointer pointer)
|
||||
{
|
||||
velocity.y = -velocity.y;
|
||||
entity.angularVelocity *= area.friction;
|
||||
audio->play(data.sounds[SOUND_BOUNCE]);
|
||||
audio.play(character.sounds[SOUND_BOUNCE]);
|
||||
}
|
||||
velocity.y *= area.friction;
|
||||
}
|
||||
@@ -319,276 +305,14 @@ void Play::items_update(Pointer pointer)
|
||||
position = {std::clamp(position.x, bounds.x, right), std::clamp(position.y, bounds.y, bottom)};
|
||||
}
|
||||
|
||||
if (moved >= 0 && moved < (int)items.size() - 1)
|
||||
if (moved >= 0 && moved < (int)list.size() - 1)
|
||||
{
|
||||
ItemEntity entity = std::move(items[moved]);
|
||||
items.erase(items.begin() + moved);
|
||||
items.push_back(std::move(entity));
|
||||
heldItem = (int)items.size() - 1;
|
||||
ItemEntity entity = std::move(list[moved]);
|
||||
list.erase(list.begin() + moved);
|
||||
list.push_back(std::move(entity));
|
||||
held = (int)list.size() - 1;
|
||||
}
|
||||
if (wasItemHovered && !isItemHovered)
|
||||
active.play(data.cursorIdle);
|
||||
itemPointerPrevious = pointer.position;
|
||||
}
|
||||
|
||||
void Play::interact_update(Pointer pointer)
|
||||
{
|
||||
Character& data = *character;
|
||||
Actor& active = pointer_actor_get(*this);
|
||||
if (text.isScene)
|
||||
{
|
||||
isInteracting = isHoldInteracting = isHovering = false;
|
||||
interactArea = -1;
|
||||
return;
|
||||
}
|
||||
bool wasInteracting = isInteracting;
|
||||
bool wasHolding = isHoldInteracting;
|
||||
bool wasHovering = isHovering;
|
||||
isHovering = false;
|
||||
isHoldInteracting &= isInteracting;
|
||||
dialogueCooldowns.resize(data.interactAreas.size());
|
||||
for (int& cooldown : dialogueCooldowns)
|
||||
cooldown = text.entry ? std::max(0, cooldown - 1) : 0;
|
||||
|
||||
for (int i = 0; i < (int)data.interactAreas.size(); i++)
|
||||
{
|
||||
InteractArea& area = data.interactAreas[i];
|
||||
Rect rect = actor.null_rect_get(area.null);
|
||||
bool isInside = is_point_in_rect(rect, pointer.position);
|
||||
if (cursorState == CURSOR_DEFAULT && isInside && !pointer.isCaptured && area.type == pointer.type)
|
||||
{
|
||||
cursorState = CURSOR_HOVER;
|
||||
active.play(area.cursorHover, TRANSITION_IF_IDLE);
|
||||
isHovering = true;
|
||||
interactArea = i;
|
||||
bool isHoldStart = area.isHold && (pointer.isClicked || (pointer.isDown && !isInteracting));
|
||||
if (isHoldStart || (!area.isHold && pointer.isClicked))
|
||||
{
|
||||
if (area.isHold)
|
||||
isInteracting = isHoldInteracting = true;
|
||||
else
|
||||
{
|
||||
cursorState = CURSOR_ACTION;
|
||||
active.play(area.cursorActive, TRANSITION_FORCE, INTERRUPT_ALWAYS);
|
||||
}
|
||||
audio->play(area.sounds);
|
||||
idle_override_set(area.idleOnEnd);
|
||||
particles_spawn(area.particles, pointer.position);
|
||||
if (area.digestionClick > 0 && progress.calories > 0 && !progress.isDigesting)
|
||||
progress.digestionProgress += area.digestionClick;
|
||||
if (area.layer >= 0)
|
||||
actor.overrides.push_back({.amplitude = {area.scaleAmplitude, area.scaleAmplitude},
|
||||
.cycles = area.scaleCycles,
|
||||
.time = area.scaleTime,
|
||||
.timeStart = area.scaleTime,
|
||||
.id = area.layer});
|
||||
bool isReady = dialogueCooldowns[i] <= 0;
|
||||
dialogueCooldowns[i] = std::max(0, area.dialogueCooldown);
|
||||
bool isFull = is_over_capacity() && data.dialogue.pools.contains(area.dialoguePoolFull);
|
||||
const std::string& pool = isFull ? area.dialoguePoolFull : area.dialoguePool;
|
||||
if (data.dialogue.pools.contains(pool))
|
||||
{
|
||||
if (text.is_interruptible() && isReady)
|
||||
text.set(*this, data.dialogue.pool_get(pool), TEXT_INTERRUPTIBLE,
|
||||
area.isHold ? "" : area.idleOnEnd);
|
||||
}
|
||||
else if (area.dialoguePool.empty() && !area.animation.empty())
|
||||
body_play(area.animation, TRANSITION_FORCE);
|
||||
}
|
||||
if (isInteracting)
|
||||
{
|
||||
cursorState = CURSOR_ACTION;
|
||||
active.play(area.cursorActive);
|
||||
Vec2 moved = pointer.position - pointerPrevious;
|
||||
if (area.digestionHover > 0 && progress.calories > 0 && !progress.isDigesting &&
|
||||
(moved.x != 0 || moved.y != 0))
|
||||
progress.digestionProgress += area.digestionHover;
|
||||
}
|
||||
}
|
||||
if ((i == interactArea && !isInside) || pointer.isReleased || pointer.isCaptured)
|
||||
{
|
||||
isInteracting = isHoldInteracting = false;
|
||||
interactArea = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if ((wasInteracting && !isInteracting) || (wasHovering && !isHovering))
|
||||
active.play(data.cursorIdle);
|
||||
if (wasHolding && !isHoldInteracting && !actor.pending)
|
||||
idle_play();
|
||||
if (wasHovered && !isHovered)
|
||||
active.play(character.cursor.idle);
|
||||
pointerPrevious = pointer.position;
|
||||
if (isJustDigested && text.is_interruptible())
|
||||
text.set(*this, data.dialogue.hook_get("Digest"));
|
||||
if (isJustStageUp)
|
||||
text.set(*this, data.dialogue.hook_get("StageUp"));
|
||||
}
|
||||
|
||||
// Moves the cursor toward a target, braking to land on it. Returns whether it's there.
|
||||
static bool is_target_reached(Vec2& position, Vec2& velocity, Vec2 target, float speed, float acceleration)
|
||||
{
|
||||
Vec2 delta = target - position;
|
||||
float distance = vec2_length(delta);
|
||||
if (!std::isfinite(distance))
|
||||
return false;
|
||||
if (distance <= AUTO_TARGET_EPSILON)
|
||||
{
|
||||
position = target;
|
||||
velocity = {};
|
||||
return true;
|
||||
}
|
||||
Vec2 desired = delta / distance * std::min(speed, std::sqrt(AUTO_BRAKING * acceleration * distance));
|
||||
Vec2 change = desired - velocity;
|
||||
float changeLength = vec2_length(change);
|
||||
velocity = changeLength > acceleration ? velocity + change / changeLength * acceleration : desired;
|
||||
if (vec2_length(velocity) >= distance)
|
||||
{
|
||||
position = target;
|
||||
velocity = {};
|
||||
return true;
|
||||
}
|
||||
position = position + velocity;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_autofeed_food(const Item& item)
|
||||
{
|
||||
return item.use == USE_EDIBLE && item.capacityBonus >= 0 && item.digestionBonus >= 0;
|
||||
}
|
||||
|
||||
static bool is_edible(Play& play, const Item& item)
|
||||
{
|
||||
return is_autofeed_food(item) && item.calories > 0 && play.progress.calories + item.calories <= play.capacity_max_get();
|
||||
}
|
||||
|
||||
// The edible item in the world with the most calories, ties broken at random.
|
||||
static int food_best_get(Play& play)
|
||||
{
|
||||
int best = -1;
|
||||
int ties = 0;
|
||||
for (int i = 0; i < (int)play.items.size(); i++)
|
||||
{
|
||||
Item& item = play.character->items[play.items[i].id];
|
||||
if (!is_edible(play, item))
|
||||
continue;
|
||||
float bestCalories = best >= 0 ? play.character->items[play.items[best].id].calories : -1;
|
||||
if (item.calories > bestCalories)
|
||||
best = i, ties = 1;
|
||||
else if (item.calories == bestCalories && SDL_rand(++ties) == 0)
|
||||
best = i;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// Plays for the player: spawns food, carries it to the mouth, and rubs or pats the belly along when nothing fits.
|
||||
void Play::autofeed_update(Pointer& itemPointer, Pointer& interactPointer)
|
||||
{
|
||||
Character& data = *character;
|
||||
isAutofeedActive = false;
|
||||
autoCursor.isVisible = false;
|
||||
auto reset = [&]() {
|
||||
isAutoItemHeld = isAutoDigestHeld = false;
|
||||
autoTarget = -1;
|
||||
autoClickCooldown = 0;
|
||||
autoVelocity = {};
|
||||
};
|
||||
bool isWorldFood = false;
|
||||
bool isAnyEdible = false;
|
||||
for (ItemEntity& entity : items)
|
||||
{
|
||||
isWorldFood |= is_autofeed_food(data.items[entity.id]);
|
||||
isAnyEdible |= is_edible(*this, data.items[entity.id]);
|
||||
}
|
||||
if (!isAutofeed || isStageUp || isJustStageUp || isJustStageFinal)
|
||||
return reset();
|
||||
spawn_queue_add();
|
||||
if (progress.calories <= AUTO_CALORIE_EPSILON && !isWorldFood)
|
||||
return reset();
|
||||
|
||||
if (autoCursor.position.x == 0 && autoCursor.position.y == 0)
|
||||
autoCursor.position = actor.position;
|
||||
Rect bodyBounds = actor.bounds_get();
|
||||
float speedScale = is_rect_valid(bodyBounds) ? std::clamp(std::sqrt(bodyBounds.w * bodyBounds.h / AUTO_SCALE_AREA),
|
||||
AUTO_SCALE_MIN, AUTO_SCALE_MAX)
|
||||
: 1;
|
||||
Rect mouth = {NAN, NAN, NAN, NAN};
|
||||
for (EatArea& eatArea : data.eatAreas)
|
||||
if (!is_rect_valid(mouth))
|
||||
mouth = eat_rect_get(eatArea);
|
||||
bool isMouth = is_rect_valid(mouth);
|
||||
auto move = [&](Vec2 target, float speed, float acceleration) {
|
||||
isAutofeedActive = autoCursor.isVisible = true;
|
||||
itemPointer = interactPointer = {.position = autoCursor.position, .isAutomated = true};
|
||||
bool isThere = is_target_reached(autoCursor.position, autoVelocity, target, speed * speedScale,
|
||||
acceleration * speedScale);
|
||||
itemPointer.position = interactPointer.position = autoCursor.position;
|
||||
return isThere;
|
||||
};
|
||||
|
||||
if (heldItem >= 0 && heldItem < (int)items.size())
|
||||
{
|
||||
if (isMouth && is_edible(*this, data.items[items[heldItem].id]))
|
||||
{
|
||||
move(rect_middle(mouth), AUTO_SPEED, AUTO_ACCELERATION);
|
||||
itemPointer.isDown = isAutoItemHeld = true;
|
||||
isAutoDigestHeld = false;
|
||||
return;
|
||||
}
|
||||
move(autoCursor.position, AUTO_SPEED, AUTO_ACCELERATION);
|
||||
itemPointer.isReleased = true;
|
||||
isAutoItemHeld = false;
|
||||
autoTarget = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
bool isTargetValid = autoTarget >= 0 && autoTarget < (int)items.size() &&
|
||||
is_edible(*this, data.items[items[autoTarget].id]);
|
||||
if (!isMouth || !isTargetValid)
|
||||
autoTarget = isMouth ? food_best_get(*this) : -1;
|
||||
if (autoTarget >= 0)
|
||||
{
|
||||
Rect rect = items[autoTarget].actor.bounds_get();
|
||||
if (!is_rect_valid(rect))
|
||||
return reset();
|
||||
move(rect_middle(rect), AUTO_SPEED, AUTO_ACCELERATION);
|
||||
bool isInside = is_point_in_rect(rect, autoCursor.position);
|
||||
itemPointer.target = autoTarget;
|
||||
itemPointer.isClicked = isInside && !isAutoItemHeld;
|
||||
itemPointer.isDown = isInside || isAutoItemHeld;
|
||||
isAutoItemHeld |= isInside;
|
||||
isAutoDigestHeld = false;
|
||||
return;
|
||||
}
|
||||
if (!(isWorldFood || !isAnyEdible))
|
||||
return reset();
|
||||
|
||||
InteractArea* digestArea = nullptr;
|
||||
for (InteractArea& area : data.interactAreas)
|
||||
if (!digestArea && (area.digestionClick > 0 || area.digestionHover > 0))
|
||||
digestArea = &area;
|
||||
Rect rect = digestArea ? actor.null_rect_get(digestArea->null) : Rect{NAN, NAN, NAN, NAN};
|
||||
if (!is_rect_valid(rect))
|
||||
return reset();
|
||||
autoAngle += AUTO_ANGLE_STEP;
|
||||
float radius = std::max(1.0f, std::min(rect.w, rect.h) * AUTO_DIGEST_RADIUS);
|
||||
Vec2 target = rect_middle(rect) + Vec2{std::cos(autoAngle), std::sin(autoAngle)} * radius;
|
||||
bool isThere = move(target, AUTO_DIGEST_SPEED, AUTO_DIGEST_ACCELERATION);
|
||||
interactPointer.type = digestArea->type;
|
||||
if (digestArea->digestionHover > 0)
|
||||
{
|
||||
bool isInside = is_point_in_rect(rect, autoCursor.position);
|
||||
interactPointer.isClicked = isInside && !isAutoDigestHeld;
|
||||
interactPointer.isDown = isInside;
|
||||
interactPointer.isReleased = isAutoDigestHeld && !isInside;
|
||||
isAutoDigestHeld = isInside;
|
||||
}
|
||||
else
|
||||
{
|
||||
autoClickCooldown = std::max(0, autoClickCooldown - 1);
|
||||
interactPointer.isClicked = isThere && autoClickCooldown <= 0 && !isAutoDigestHeld;
|
||||
interactPointer.isReleased = isAutoDigestHeld;
|
||||
if (interactPointer.isClicked)
|
||||
autoClickCooldown = AUTO_CLICK_COOLDOWN;
|
||||
isAutoDigestHeld = interactPointer.isClicked;
|
||||
}
|
||||
isAutoItemHeld = false;
|
||||
}
|
||||
+104
-99
@@ -3,7 +3,6 @@
|
||||
#include "ui.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_main.h>
|
||||
#include <utility>
|
||||
#include <imgui_impl_opengl3.h>
|
||||
#include <imgui_impl_sdl3.h>
|
||||
|
||||
@@ -29,11 +28,13 @@ enum Screen
|
||||
SCREEN_PLAY
|
||||
};
|
||||
|
||||
// Everything the game holds, from start to exit.
|
||||
// Everything the game holds, from start to exit. Long-lived parts keep pointers to the shared ones (the audio,
|
||||
// renderer and command list) set here once.
|
||||
struct App
|
||||
{
|
||||
std::vector<Character> characters;
|
||||
Audio audio;
|
||||
Commands commands;
|
||||
Console console;
|
||||
Play play;
|
||||
Renderer renderer;
|
||||
@@ -44,55 +45,54 @@ struct App
|
||||
SDL_Window* window = nullptr;
|
||||
Uint64 tickPrevious = 0;
|
||||
Screen screen = SCREEN_SELECT;
|
||||
bool isCursorHidden = false;
|
||||
bool isRunning = true;
|
||||
};
|
||||
|
||||
static App game;
|
||||
static App app;
|
||||
|
||||
static int character_get(const std::string& key)
|
||||
{
|
||||
for (int i = 0; i < (int)game.characters.size(); i++)
|
||||
if (game.characters[i].key == key)
|
||||
for (int i = 0; i < (int)app.characters.size(); i++)
|
||||
if (app.characters[i].key == key)
|
||||
return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void play_exit()
|
||||
{
|
||||
if (game.screen != SCREEN_PLAY)
|
||||
if (app.screen != SCREEN_PLAY)
|
||||
return;
|
||||
game.play.save();
|
||||
game.settings.lastCharacter.clear();
|
||||
ImGui::GetIO().FontDefault = game.font;
|
||||
style_set(game.settings.color);
|
||||
widget_sounds_set(nullptr, nullptr, nullptr);
|
||||
game.screen = SCREEN_SELECT;
|
||||
app.play.save();
|
||||
app.settings.lastCharacter.clear();
|
||||
ImGui::GetIO().FontDefault = app.font;
|
||||
style_set(app.settings.color);
|
||||
app.screen = SCREEN_SELECT;
|
||||
}
|
||||
|
||||
static void play_start(int index, Game mode)
|
||||
static void play_start(const std::string& key, Game game)
|
||||
{
|
||||
int index = character_get(key);
|
||||
if (index < 0)
|
||||
return;
|
||||
play_exit();
|
||||
Character& character = game.characters[index];
|
||||
game.ui.characterIndex = index;
|
||||
game.play.set(character, mode, game.renderer, game.audio);
|
||||
game.settings.lastCharacter = character.key;
|
||||
ImGui::GetIO().FontDefault = character.font ? character.font : game.font;
|
||||
style_set(game.settings.isCharacterColor ? character.color : game.settings.color, character.rounding);
|
||||
widget_sounds_set(&game.audio, &character.sounds[SOUND_HOVER], &character.sounds[SOUND_SELECT]);
|
||||
game.ui.itemActors.clear();
|
||||
game.ui.selectedItem = -1;
|
||||
game.screen = SCREEN_PLAY;
|
||||
Character& character = app.characters[index];
|
||||
app.ui.characterIndex = index;
|
||||
app.play.set(character, game, app.renderer, app.audio);
|
||||
app.settings.lastCharacter = character.key;
|
||||
ImGui::GetIO().FontDefault = character.menu.font ? character.menu.font : app.font;
|
||||
style_set(app.settings.isCharacterColor ? character.color : app.settings.color, character.menu.rounding);
|
||||
app.screen = SCREEN_PLAY;
|
||||
}
|
||||
|
||||
static void characters_reload()
|
||||
{
|
||||
std::string key = game.screen == SCREEN_PLAY ? game.play.character->key : "";
|
||||
std::string key = app.screen == SCREEN_PLAY ? app.play.character->key : "";
|
||||
play_exit();
|
||||
game.ui.previewActor = {};
|
||||
game.characters = characters_load(game.renderer);
|
||||
play_start(character_get(key), GAME_CONTINUE);
|
||||
app.ui.previewActor = {};
|
||||
app.ui.catalogCharacter = nullptr;
|
||||
app.characters = characters_load(app.renderer);
|
||||
play_start(key, GAME_CONTINUE);
|
||||
}
|
||||
|
||||
static void init()
|
||||
@@ -100,29 +100,29 @@ static void init()
|
||||
if (!SDL_Init(SDL_INIT_VIDEO))
|
||||
{
|
||||
SDL_Log("Failed to initialize SDL: %s", SDL_GetError());
|
||||
game.isRunning = false;
|
||||
app.isRunning = false;
|
||||
return;
|
||||
}
|
||||
game.settings.load();
|
||||
app.settings.load();
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, GL_MAJOR);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, GL_MINOR);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, GL_PROFILE);
|
||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||
game.window = SDL_CreateWindow(GAME_NAME, game.settings.width, game.settings.height,
|
||||
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
|
||||
SDL_SetWindowPosition(game.window, game.settings.x, game.settings.y);
|
||||
game.context = game.window ? SDL_GL_CreateContext(game.window) : nullptr;
|
||||
if (game.context)
|
||||
game.renderer.init();
|
||||
if (!game.renderer.isValid)
|
||||
app.window =
|
||||
SDL_CreateWindow(GAME_NAME, app.settings.width, app.settings.height, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
|
||||
SDL_SetWindowPosition(app.window, app.settings.x, app.settings.y);
|
||||
app.context = app.window ? SDL_GL_CreateContext(app.window) : nullptr;
|
||||
if (app.context)
|
||||
app.renderer.init();
|
||||
if (!app.renderer.isValid)
|
||||
{
|
||||
SDL_Log("Failed to initialize OpenGL: %s", SDL_GetError());
|
||||
game.isRunning = false;
|
||||
app.isRunning = false;
|
||||
return;
|
||||
}
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
game.audio.init();
|
||||
game.audio.gain_set(game.settings.isMuted ? 0 : game.settings.volume / PERCENT);
|
||||
app.audio.init();
|
||||
app.audio.gain_set(app.settings.isMuted ? 0 : app.settings.volume / PERCENT);
|
||||
|
||||
ImGui::CreateContext();
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
@@ -131,14 +131,19 @@ static void init()
|
||||
io.KeyRepeatDelay = KEY_REPEAT_DELAY;
|
||||
io.KeyRepeatRate = KEY_REPEAT_RATE;
|
||||
ImGui::StyleColorsDark();
|
||||
ImGui_ImplSDL3_InitForOpenGL(game.window, game.context);
|
||||
ImGui_ImplSDL3_InitForOpenGL(app.window, app.context);
|
||||
ImGui_ImplOpenGL3_Init(GLSL_VERSION);
|
||||
game.font = io.Fonts->AddFontFromFileTTF(path_get("font/font.ttf").c_str(), FONT_NORMAL);
|
||||
io.FontDefault = game.font;
|
||||
style_set(game.settings.color);
|
||||
app.font = io.Fonts->AddFontFromFileTTF(path_get("font/font.ttf").c_str(), FONT_NORMAL);
|
||||
io.FontDefault = app.font;
|
||||
style_set(app.settings.color);
|
||||
|
||||
game.characters = characters_load(game.renderer);
|
||||
play_start(character_get(game.settings.lastCharacter), GAME_CONTINUE);
|
||||
app.ui.audio = &app.audio;
|
||||
app.ui.renderer = &app.renderer;
|
||||
app.ui.commands = &app.commands;
|
||||
app.console.renderer = &app.renderer;
|
||||
app.console.commands = &app.commands;
|
||||
app.characters = characters_load(app.renderer);
|
||||
play_start(app.settings.lastCharacter, GAME_CONTINUE);
|
||||
}
|
||||
|
||||
static void events_handle()
|
||||
@@ -148,94 +153,94 @@ static void events_handle()
|
||||
{
|
||||
ImGui_ImplSDL3_ProcessEvent(&event);
|
||||
if (event.type == SDL_EVENT_QUIT)
|
||||
game.isRunning = false;
|
||||
app.isRunning = false;
|
||||
if (event.type != SDL_EVENT_KEY_DOWN || event.key.repeat)
|
||||
continue;
|
||||
bool isTyping = ImGui::GetIO().WantTextInput;
|
||||
bool isAltEnter = event.key.key == SDLK_RETURN && (event.key.mod & SDL_KMOD_ALT);
|
||||
if (event.key.key == SDLK_F11 || isAltEnter || (!isTyping && event.key.scancode == SDL_SCANCODE_F))
|
||||
game.ui.isFullscreenToggled = true;
|
||||
app.commands.push_back({"", COMMAND_FULLSCREEN});
|
||||
if (!isTyping && event.key.scancode == SDL_SCANCODE_M)
|
||||
{
|
||||
game.settings.isMuted = !game.settings.isMuted;
|
||||
game.audio.gain_set(game.settings.isMuted ? 0 : game.settings.volume / PERCENT);
|
||||
app.settings.isMuted = !app.settings.isMuted;
|
||||
app.audio.gain_set(app.settings.isMuted ? 0 : app.settings.volume / PERCENT);
|
||||
}
|
||||
if (event.key.scancode == SDL_SCANCODE_GRAVE && game.settings.isConsole)
|
||||
game.console.isOpen = !game.console.isOpen;
|
||||
if (event.key.scancode == SDL_SCANCODE_GRAVE && app.settings.isConsole)
|
||||
app.console.isOpen = !app.console.isOpen;
|
||||
}
|
||||
}
|
||||
|
||||
// Carries out what the UI, console and shortcuts asked for this tick.
|
||||
static void commands_handle()
|
||||
{
|
||||
Commands commands = std::move(app.commands);
|
||||
app.commands.clear();
|
||||
for (Command& command : commands)
|
||||
{
|
||||
if (command.type == COMMAND_CONTINUE || command.type == COMMAND_NEW_GAME)
|
||||
play_start(command.character, command.type == COMMAND_NEW_GAME ? GAME_NEW : GAME_CONTINUE);
|
||||
if (command.type == COMMAND_MENU)
|
||||
play_exit();
|
||||
if (command.type == COMMAND_SAVE && app.screen == SCREEN_PLAY)
|
||||
app.play.save();
|
||||
if (command.type == COMMAND_RELOAD)
|
||||
characters_reload();
|
||||
if (command.type == COMMAND_FULLSCREEN)
|
||||
SDL_SetWindowFullscreen(app.window, !(SDL_GetWindowFlags(app.window) & SDL_WINDOW_FULLSCREEN));
|
||||
}
|
||||
}
|
||||
|
||||
static void update()
|
||||
{
|
||||
Play& play = game.play;
|
||||
Ui& ui = game.ui;
|
||||
Vec2 screen = {ImGui::GetIO().DisplaySize.x, ImGui::GetIO().DisplaySize.y};
|
||||
if (game.screen == SCREEN_SELECT)
|
||||
{
|
||||
ui.select_update(game.characters, game.settings, game.renderer, game.audio);
|
||||
if (ui.isRefresh)
|
||||
characters_reload();
|
||||
if (ui.isNewGame || ui.isContinue)
|
||||
play_start(ui.characterIndex, ui.isNewGame ? GAME_NEW : GAME_CONTINUE);
|
||||
ui.isNewGame = ui.isContinue = false;
|
||||
}
|
||||
ImVec2 display = ImGui::GetIO().DisplaySize;
|
||||
if (app.screen == SCREEN_SELECT)
|
||||
app.ui.select_update(app.characters, app.settings);
|
||||
else
|
||||
{
|
||||
ui.play_update(play, game.settings, game.renderer, game.audio);
|
||||
play.update(screen);
|
||||
if (std::exchange(ui.isSave, false))
|
||||
play.save();
|
||||
if (std::exchange(ui.isReturn, false))
|
||||
play_exit();
|
||||
app.ui.play_update(app.play, app.settings);
|
||||
app.play.update({display.x, display.y});
|
||||
}
|
||||
app.console.update(app.screen == SCREEN_PLAY ? &app.play : nullptr, app.characters);
|
||||
commands_handle();
|
||||
|
||||
Console& console = game.console;
|
||||
console.update(game.screen == SCREEN_PLAY ? &play : nullptr, game.characters);
|
||||
if (console.action == ACTION_MENU)
|
||||
play_exit();
|
||||
if (console.action == ACTION_CHARACTER || console.action == ACTION_NEW_GAME)
|
||||
play_start(character_get(console.target), console.action == ACTION_NEW_GAME ? GAME_NEW : GAME_CONTINUE);
|
||||
if (console.action == ACTION_RELOAD)
|
||||
characters_reload();
|
||||
|
||||
if (std::exchange(ui.isFullscreenToggled, false))
|
||||
SDL_SetWindowFullscreen(game.window, !(SDL_GetWindowFlags(game.window) & SDL_WINDOW_FULLSCREEN));
|
||||
ui.isFullscreen = SDL_GetWindowFlags(game.window) & SDL_WINDOW_FULLSCREEN;
|
||||
if (!ui.isFullscreen)
|
||||
app.ui.isFullscreen = SDL_GetWindowFlags(app.window) & SDL_WINDOW_FULLSCREEN;
|
||||
if (!app.ui.isFullscreen)
|
||||
{
|
||||
SDL_GetWindowSize(game.window, &game.settings.width, &game.settings.height);
|
||||
SDL_GetWindowPosition(game.window, &game.settings.x, &game.settings.y);
|
||||
SDL_GetWindowSize(app.window, &app.settings.width, &app.settings.height);
|
||||
SDL_GetWindowPosition(app.window, &app.settings.x, &app.settings.y);
|
||||
}
|
||||
bool isCursorHidden = game.screen == SCREEN_PLAY && play.character->cursor && !console.isOpen;
|
||||
isCursorHidden ? SDL_HideCursor() : SDL_ShowCursor();
|
||||
bool isCursorHidden = app.screen == SCREEN_PLAY && app.play.cursor.anm2 && !app.console.isOpen;
|
||||
if (isCursorHidden != app.isCursorHidden)
|
||||
isCursorHidden ? SDL_HideCursor() : SDL_ShowCursor();
|
||||
app.isCursorHidden = isCursorHidden;
|
||||
}
|
||||
|
||||
static void render()
|
||||
{
|
||||
Play& play = game.play;
|
||||
Renderer& renderer = game.renderer;
|
||||
bool isPlaying = game.screen == SCREEN_PLAY;
|
||||
Play& play = app.play;
|
||||
Renderer& renderer = app.renderer;
|
||||
bool isPlaying = app.screen == SCREEN_PLAY;
|
||||
ImVec2 screen = ImGui::GetIO().DisplaySize;
|
||||
Color background = isPlaying && game.settings.isCharacterColor ? play.character->color : game.settings.color;
|
||||
Color background = isPlaying && app.settings.isCharacterColor ? play.character->color : app.settings.color;
|
||||
renderer.canvas_set(nullptr, (int)screen.x, (int)screen.y, background);
|
||||
if (isPlaying)
|
||||
play.render(renderer, {screen.x, screen.y});
|
||||
renderer.flush();
|
||||
ImGui::Render();
|
||||
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
|
||||
if (isPlaying && play.character->cursor && !game.console.isOpen)
|
||||
if (app.isCursorHidden)
|
||||
play.cursor.render(renderer, {});
|
||||
renderer.flush();
|
||||
SDL_GL_SwapWindow(game.window);
|
||||
SDL_GL_SwapWindow(app.window);
|
||||
}
|
||||
|
||||
// Runs a tick (sixty a second) once its time comes.
|
||||
static void loop()
|
||||
{
|
||||
Uint64 now = SDL_GetTicks();
|
||||
if (now - game.tickPrevious < TICK_MILLISECONDS)
|
||||
if (now - app.tickPrevious < TICK_MILLISECONDS)
|
||||
return SDL_Delay(1);
|
||||
game.tickPrevious = now;
|
||||
app.tickPrevious = now;
|
||||
events_handle();
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
@@ -247,24 +252,24 @@ static void loop()
|
||||
static void quit()
|
||||
{
|
||||
play_exit();
|
||||
game.settings.save();
|
||||
app.settings.save();
|
||||
ImGui_ImplOpenGL3_Shutdown();
|
||||
ImGui_ImplSDL3_Shutdown();
|
||||
ImGui::DestroyContext();
|
||||
SDL_GL_DestroyContext(game.context);
|
||||
SDL_DestroyWindow(game.window);
|
||||
SDL_GL_DestroyContext(app.context);
|
||||
SDL_DestroyWindow(app.window);
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
int main(int, char**)
|
||||
{
|
||||
init();
|
||||
if (!game.isRunning)
|
||||
if (!app.isRunning)
|
||||
return 1;
|
||||
#ifdef SDL_PLATFORM_EMSCRIPTEN
|
||||
emscripten_set_main_loop(loop, 0, true);
|
||||
#else
|
||||
while (game.isRunning)
|
||||
while (app.isRunning)
|
||||
loop();
|
||||
#endif
|
||||
quit();
|
||||
|
||||
+251
-463
@@ -4,10 +4,13 @@
|
||||
#include <utility>
|
||||
|
||||
constexpr Rect WORLD_BOUNDS = {96, 54, 1728, 972};
|
||||
constexpr ImGuiKey CHEAT_CODE[] = {ImGuiKey_UpArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_DownArrow,
|
||||
ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow,
|
||||
ImGuiKey_B, ImGuiKey_A};
|
||||
constexpr const char* AUTOFEED_TYPE = "Rub";
|
||||
constexpr ImGuiKey CHEAT_CODE[] = {ImGuiKey_UpArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_DownArrow,
|
||||
ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow,
|
||||
ImGuiKey_B, ImGuiKey_A};
|
||||
constexpr ImGuiKey CHEAT_KEYS[] = {ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_LeftArrow,
|
||||
ImGuiKey_RightArrow, ImGuiKey_B, ImGuiKey_A};
|
||||
constexpr const char* KEY_DISCOVERED = "discovered";
|
||||
constexpr const char* KEY_ITEM = "item";
|
||||
constexpr float AUTOSAVE_TIME = 30;
|
||||
constexpr float CHEAT_TIME = 5;
|
||||
constexpr float FOCUS_MENU_WIDTH = 0.3f;
|
||||
@@ -17,18 +20,14 @@ constexpr float SHAKE_DECAY = 0.97f;
|
||||
constexpr float SHAKE_EPSILON = 0.1f;
|
||||
constexpr float SHAKE_LERP = 0.72f;
|
||||
constexpr float SHAKE_MAGNITUDE = 0.1f;
|
||||
constexpr float TALK_STEP = 0.5f;
|
||||
constexpr int OVERRIDE_TALK = 0;
|
||||
constexpr int OVERRIDE_BLINK = 1;
|
||||
constexpr int OVERRIDE_EXPAND = 2;
|
||||
|
||||
// Keeps a value within a range given either way round, putting what isn't a number at its low end.
|
||||
static float value_clamp(float value, float low, float high)
|
||||
struct SavedItem
|
||||
{
|
||||
if (low > high)
|
||||
std::swap(low, high);
|
||||
return std::isfinite(value) ? std::clamp(value, low, high) : low;
|
||||
}
|
||||
std::string name;
|
||||
Vec2 position;
|
||||
Vec2 velocity;
|
||||
float rotation = 0;
|
||||
};
|
||||
|
||||
static int zoom_index_get(float zoom)
|
||||
{
|
||||
@@ -39,116 +38,99 @@ static int zoom_index_get(float zoom)
|
||||
return index;
|
||||
}
|
||||
|
||||
// Starts a character, from its save unless it's a new game. A save is the body's stats, the play's own fields (see
|
||||
// visit), the items found (one "discovered" line each) and those lying in the world ("item" lines of position,
|
||||
// velocity, rotation and name).
|
||||
void Play::set(Character& played, Game game, Renderer& renderer, Audio& sound)
|
||||
{
|
||||
played.load(renderer, sound);
|
||||
*this = Play();
|
||||
character = &played;
|
||||
audio = &sound;
|
||||
camera.zoom = 0;
|
||||
|
||||
std::vector<std::string> found;
|
||||
std::vector<SavedItem> saved;
|
||||
if (game == GAME_CONTINUE)
|
||||
progress.load(played.save_path_get());
|
||||
if (!progress.isValid)
|
||||
{
|
||||
progress.weight = played.weight;
|
||||
progress.capacity = played.capacity;
|
||||
progress.digestionRate = played.digestionRate;
|
||||
}
|
||||
is_file_read(played.save_path_get(), [&](const std::string& key, const char* value) {
|
||||
field_line_read(body.stats, key, value);
|
||||
field_line_read(*this, key, value);
|
||||
SavedItem item;
|
||||
int length = 0;
|
||||
if (key == KEY_DISCOVERED)
|
||||
found.emplace_back(value);
|
||||
else if (key == KEY_ITEM && std::sscanf(value, "%f %f %f %f %f %n", &item.position.x, &item.position.y,
|
||||
&item.velocity.x, &item.velocity.y, &item.rotation, &length) == 5)
|
||||
saved.push_back({value + length, item.position, item.velocity, item.rotation});
|
||||
});
|
||||
body.set(played, sound, game);
|
||||
area_set();
|
||||
|
||||
actor = Actor(played.anm2);
|
||||
actor.overrides.resize(OVERRIDE_EXPAND);
|
||||
actor.overrides[OVERRIDE_TALK].id = played.talkDestination;
|
||||
actor.overrides[OVERRIDE_BLINK].id = played.blinkDestination;
|
||||
for (ExpandArea& expandArea : played.expandAreas)
|
||||
Catalog& catalog = played.catalog;
|
||||
items.discovered.assign(catalog.items.size(), false);
|
||||
for (const std::string& name : found)
|
||||
if (int id = catalog.item_get(name); id >= 0)
|
||||
items.discovered[id] = true;
|
||||
for (SavedItem& item : saved)
|
||||
{
|
||||
actor.overrides.push_back({.id = expandArea.layer});
|
||||
actor.overrides.push_back({.id = expandArea.null, .target = TARGET_NULL});
|
||||
int id = catalog.item_get(item.name);
|
||||
if (id >= 0)
|
||||
items.add(*this, id, item.position, item.velocity, item.rotation);
|
||||
else
|
||||
SDL_Log("Saved item not found: %s", item.name.c_str());
|
||||
}
|
||||
values_clamp();
|
||||
stage = stage_get(progress.weight);
|
||||
|
||||
bool isRolled = game == GAME_NEW && played.alternateTexture && is_chance(played.alternateChance);
|
||||
progress.isAlternate |= isRolled;
|
||||
if (isRolled)
|
||||
audio->play(played.alternateSound);
|
||||
if (progress.isAlternate && played.alternateSheet < (int)actor.sheets.size())
|
||||
actor.sheets[played.alternateSheet] = played.alternateTexture;
|
||||
|
||||
cursorType = played.interactTypes.empty() ? -1 : 0;
|
||||
if (played.cursor)
|
||||
if (played.cursor.anm2)
|
||||
{
|
||||
cursor = Actor(played.cursor);
|
||||
autoCursor = Actor(played.cursor);
|
||||
cursor = Actor(played.cursor.anm2);
|
||||
autoCursor = Actor(played.cursor.anm2);
|
||||
autoCursor.isVisible = false;
|
||||
}
|
||||
|
||||
spawnDelay = progress.spawnDelay;
|
||||
discovered.assign(played.items.size(), false);
|
||||
auto item_get = [&](const std::string& name) {
|
||||
for (int i = 0; i < (int)played.items.size(); i++)
|
||||
if (played.items[i].name == name)
|
||||
return i;
|
||||
return -1;
|
||||
};
|
||||
for (const std::string& name : progress.discovered)
|
||||
if (int id = item_get(name); id >= 0)
|
||||
discovered[id] = true;
|
||||
for (SavedItem& saved : progress.items)
|
||||
if (int id = item_get(saved.name); id >= 0)
|
||||
item_add(id, saved.position, saved.velocity, saved.rotation);
|
||||
|
||||
isPostgame = progress.isPostgame || stage >= (int)played.stages.size();
|
||||
isPostgame |= body.stage >= (int)played.stages.size();
|
||||
#ifdef DEBUG
|
||||
isCheats = true;
|
||||
#endif
|
||||
isCheats |= isPostgame;
|
||||
|
||||
int areaIndex = played.area_get(stage);
|
||||
if (areaIndex >= 0)
|
||||
area = Actor(played.areas[areaIndex].anm2);
|
||||
bounds = areaIndex >= 0 && is_rect_valid(played.areas[areaIndex].bounds) ? played.areas[areaIndex].bounds
|
||||
: WORLD_BOUNDS;
|
||||
idle_play();
|
||||
actor.update(*audio);
|
||||
isFocusPending = true;
|
||||
camera.isFocusPending = camera.zoom < ZOOM_LADDER[0];
|
||||
camera.zoom = camera.isFocusPending ? ZOOM_LADDER[0] : ZOOM_LADDER[zoom_index_get(camera.zoom)];
|
||||
if (game == GAME_NEW)
|
||||
text.set(*this, played.dialogue.hook_get("Start"), TEXT_UNINTERRUPTIBLE);
|
||||
text.set(*this, played.dialogue.hook_get(HOOK_START), TEXT_UNINTERRUPTIBLE);
|
||||
}
|
||||
|
||||
void Play::save()
|
||||
{
|
||||
if (!character)
|
||||
return;
|
||||
progress.pan = pan;
|
||||
progress.zoom = zoom;
|
||||
progress.spawnDelay = spawnDelay;
|
||||
progress.isPostgame = isPostgame;
|
||||
progress.items.clear();
|
||||
for (ItemEntity& entity : items)
|
||||
progress.items.push_back(
|
||||
{character->items[entity.id].name, entity.actor.position, entity.velocity, entity.rotation_get()});
|
||||
progress.discovered.clear();
|
||||
for (int i = 0; i < (int)discovered.size(); i++)
|
||||
if (discovered[i])
|
||||
progress.discovered.push_back(character->items[i].name);
|
||||
progress.save(character->save_path_get());
|
||||
toast_add(STRING_ToastSaving);
|
||||
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";
|
||||
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 +
|
||||
"\n";
|
||||
file_write(character->save_path_get(), text);
|
||||
toast_add("Saving...");
|
||||
}
|
||||
|
||||
void Play::toast_add(StringId message)
|
||||
// Shows the area of the body's stage, with its bounds.
|
||||
void Play::area_set()
|
||||
{
|
||||
toasts.push_back({character->strings[message], TOAST_LIFE});
|
||||
int index = character->area_get(body.stage);
|
||||
if (index >= 0 && area.anm2 != character->areas[index].anm2)
|
||||
area = Actor(character->areas[index].anm2);
|
||||
bounds = index >= 0 && is_rect_valid(character->areas[index].bounds) ? character->areas[index].bounds : WORLD_BOUNDS;
|
||||
}
|
||||
|
||||
void Play::shake_add(float magnitude, int time)
|
||||
void Play::toast_add(const std::string& message)
|
||||
{
|
||||
shakeTimeMax = std::max(1, time);
|
||||
shakeTime = shakeTimeMax;
|
||||
shake = {random_get(-magnitude, magnitude), random_get(-magnitude, magnitude)};
|
||||
toasts.push_back({message, TOAST_LIFE});
|
||||
}
|
||||
|
||||
Vec2 Play::world_get(Vec2 screen)
|
||||
Actor& Play::pointer_actor_get()
|
||||
{
|
||||
return pan + screen / (zoom / PERCENT);
|
||||
return autofeed.isActive ? autoCursor : cursor;
|
||||
}
|
||||
|
||||
Focus Play::focus_get()
|
||||
@@ -158,254 +140,209 @@ Focus Play::focus_get()
|
||||
return isMenuOpen && isToolsOpen ? FOCUS_BOTH : isMenuOpen ? FOCUS_MENU : isToolsOpen ? FOCUS_TOOLS : FOCUS_CENTER;
|
||||
}
|
||||
|
||||
// Zooms and pans so the character fills the view, leaving room for the open panels.
|
||||
void Play::focus(Vec2 screenSize)
|
||||
{
|
||||
Rect rect = actor.bounds_get();
|
||||
if (!is_rect_valid(rect))
|
||||
camera.focus(body.actor.bounds_get(), screenSize, focus_get());
|
||||
}
|
||||
|
||||
void Play::particles_spawn(const std::vector<std::string>& names, Vec2 position)
|
||||
{
|
||||
for (const std::string& name : names)
|
||||
{
|
||||
auto found = character->particles.find(name);
|
||||
if (found == character->particles.end() || !found->second.anm2)
|
||||
SDL_Log("Particle not found: %s", name.c_str());
|
||||
else
|
||||
particles.emplace_back(found->second.anm2, position, found->second.animation);
|
||||
}
|
||||
}
|
||||
|
||||
// The Konami code unlocks the cheats menu, keyed within a few seconds of each press.
|
||||
void Play::cheats_update()
|
||||
{
|
||||
if (isCheats)
|
||||
return;
|
||||
rect = rect_center(rect, FOCUS_PADDING);
|
||||
float factor = std::min(screenSize.x / rect.w, screenSize.y / rect.h);
|
||||
for (ImGuiKey key : CHEAT_KEYS)
|
||||
{
|
||||
if (!ImGui::IsKeyPressed(key, false))
|
||||
continue;
|
||||
cheatIndex = key == CHEAT_CODE[cheatIndex] ? cheatIndex + 1 : key == CHEAT_CODE[0] ? 1 : 0;
|
||||
cheatTime = (float)ImGui::GetTime();
|
||||
if (cheatIndex < (int)std::size(CHEAT_CODE))
|
||||
continue;
|
||||
isCheats = true;
|
||||
cheatIndex = 0;
|
||||
toast_add("Cheats unlocked!");
|
||||
audio->play(character->sounds[SOUND_CHEATS]);
|
||||
}
|
||||
if (cheatIndex > 0 && ImGui::GetTime() - cheatTime > CHEAT_TIME)
|
||||
cheatIndex = 0;
|
||||
}
|
||||
|
||||
// Answers what the body did this tick: dialogue for digesting and stage ups, and the ending at the final stage.
|
||||
void Play::events_handle(Vec2 screenSize)
|
||||
{
|
||||
Dialogue& dialogue = character->dialogue;
|
||||
for (Event event : events)
|
||||
{
|
||||
if (event == EVENT_DIGESTED && !text.isScene && text.is_interruptible())
|
||||
text.set(*this, dialogue.hook_get(HOOK_DIGEST));
|
||||
if (event == EVENT_STAGE_UP && !text.isScene)
|
||||
text.set(*this, dialogue.hook_get(HOOK_STAGE_UP));
|
||||
if (event != EVENT_STAGE_FINAL || isPostgame || isPostgamePending)
|
||||
continue;
|
||||
isPostgame = isCheats = !dialogue.is_hook(HOOK_END);
|
||||
if (isPostgame)
|
||||
continue;
|
||||
text.set(*this, dialogue.hook_get(HOOK_END), TEXT_UNINTERRUPTIBLE);
|
||||
isMenuOpen = isToolsOpen = false;
|
||||
body.stats.calories = 0;
|
||||
body.stats.digestionProgress = 0;
|
||||
items.drop();
|
||||
isPostgamePending = true;
|
||||
focus(screenSize);
|
||||
}
|
||||
events.clear();
|
||||
}
|
||||
|
||||
void Play::update(Vec2 screenSize)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (std::exchange(camera.isFocusPending, false))
|
||||
focus(screenSize);
|
||||
body.stats.playtime += TICK;
|
||||
cheats_update();
|
||||
text.update(*this);
|
||||
if (isPostgamePending && (!text.entry || (text.entry->isTerminal && text.isFinished)) && !text.isScene)
|
||||
{
|
||||
isMenuOpen = isPostgame = isCheats = true;
|
||||
isPostgamePending = false;
|
||||
focus(screenSize);
|
||||
}
|
||||
|
||||
if (autofeed.isEnabled)
|
||||
cursorType = autofeed.type_get(*this);
|
||||
Pointer pointer;
|
||||
pointer.position = camera.world_get({io.MousePos.x, io.MousePos.y});
|
||||
pointer.type = cursorType;
|
||||
pointer.isCaptured = io.WantCaptureMouse;
|
||||
pointer.isClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
|
||||
pointer.isDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
|
||||
pointer.isReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
||||
pointer.isRightClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
|
||||
Pointer itemPointer = pointer;
|
||||
Pointer interactPointer = pointer;
|
||||
autofeed.isActive = false;
|
||||
if (text.isScene)
|
||||
itemPointer = interactPointer = {.position = pointer.position, .isCaptured = true};
|
||||
else
|
||||
autofeed.update(*this, itemPointer, interactPointer);
|
||||
items.update(*this, itemPointer);
|
||||
interactions.update(*this, interactPointer);
|
||||
|
||||
if (cursor.anm2 && !autofeed.isActive && pointer.isClicked && !items.isHovered && !interactions.isHovering &&
|
||||
cursorState == CURSOR_DEFAULT)
|
||||
cursor.play(character->cursor.click, TRANSITION_FORCE, INTERRUPT_ALWAYS);
|
||||
|
||||
body.update(events);
|
||||
events_handle(screenSize);
|
||||
area_set();
|
||||
area.update(*audio);
|
||||
|
||||
for (int i = 0; i < (int)particles.size(); i++)
|
||||
{
|
||||
particles[i].update(*audio);
|
||||
if (!particles[i].isPlaying)
|
||||
particles.erase(particles.begin() + i--);
|
||||
}
|
||||
for (Actor* each : {&cursor, &autoCursor})
|
||||
{
|
||||
bool isActive = (each == &autoCursor) == autofeed.isActive;
|
||||
if ((!isActive || cursorState == CURSOR_DEFAULT) && !each->pending)
|
||||
each->play(character->cursor.idle, TRANSITION_IF_IDLE);
|
||||
each->update(*audio);
|
||||
}
|
||||
cursorState = CURSOR_DEFAULT;
|
||||
cursor.position = {io.MousePos.x, io.MousePos.y};
|
||||
camera.update(*this, screenSize);
|
||||
|
||||
for (int i = 0; i < (int)toasts.size(); i++)
|
||||
if (--toasts[i].life <= 0)
|
||||
toasts.erase(toasts.begin() + i--);
|
||||
if ((autosaveTime += TICK) > AUTOSAVE_TIME)
|
||||
{
|
||||
save();
|
||||
autosaveTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Play::render(Renderer& renderer, Vec2 screenSize)
|
||||
{
|
||||
Affine view = camera.view_get(screenSize);
|
||||
area.render(renderer, view);
|
||||
body.actor.render(renderer, view);
|
||||
if (!text.isScene)
|
||||
for (ItemEntity& entity : items.list)
|
||||
entity.actor.render(renderer, view);
|
||||
if (autofeed.isActive)
|
||||
autoCursor.render(renderer, view);
|
||||
for (Actor& particle : particles)
|
||||
particle.render(renderer, view);
|
||||
}
|
||||
|
||||
Vec2 Camera::world_get(Vec2 screen)
|
||||
{
|
||||
return pan + screen / (zoom / PERCENT);
|
||||
}
|
||||
|
||||
Affine Camera::view_get(Vec2 screenSize)
|
||||
{
|
||||
float scale = zoom / PERCENT;
|
||||
return {scale, 0, 0, scale, -pan.x * scale + shake.x * screenSize.x, -pan.y * scale + shake.y * screenSize.y};
|
||||
}
|
||||
|
||||
void Camera::shake_add(float magnitude, int time)
|
||||
{
|
||||
shakeTimeMax = std::max(1, time);
|
||||
shakeTime = shakeTimeMax;
|
||||
shake = {random_get(-magnitude, magnitude), random_get(-magnitude, magnitude)};
|
||||
}
|
||||
|
||||
// Zooms and pans so a target fills the view, leaving room for the open panels.
|
||||
void Camera::focus(Rect target, Vec2 screenSize, Focus panels)
|
||||
{
|
||||
if (!is_rect_valid(target))
|
||||
return;
|
||||
target = rect_center(target, FOCUS_PADDING);
|
||||
float factor = std::min(screenSize.x / target.w, screenSize.y / target.h);
|
||||
zoom = ZOOM_LADDER[zoom_index_get(std::clamp(factor * PERCENT, ZOOM_LADDER[0], std::end(ZOOM_LADDER)[-1]))];
|
||||
factor = zoom / PERCENT;
|
||||
pan = rect_middle(rect) - screenSize / factor / 2;
|
||||
Focus panels = focus_get();
|
||||
pan = rect_middle(target) - screenSize / factor / 2;
|
||||
if (panels == FOCUS_MENU || panels == FOCUS_BOTH)
|
||||
pan.x += screenSize.x * FOCUS_MENU_WIDTH / factor / 2;
|
||||
if (panels == FOCUS_TOOLS || panels == FOCUS_BOTH)
|
||||
pan.x -= screenSize.x * FOCUS_TOOLS_WIDTH / factor / 2;
|
||||
}
|
||||
|
||||
float Play::capacity_max_get()
|
||||
{
|
||||
return progress.capacity * character->overstuffedMultiplier;
|
||||
}
|
||||
|
||||
float Play::capacity_percent_get()
|
||||
{
|
||||
float capacityMax = capacity_max_get();
|
||||
return capacityMax > 0 ? progress.calories / capacityMax : 0;
|
||||
}
|
||||
|
||||
bool Play::is_over_capacity()
|
||||
{
|
||||
return progress.calories > progress.capacity;
|
||||
}
|
||||
|
||||
int Play::stage_get(float weight)
|
||||
{
|
||||
std::vector<Stage>& stages = character->stages;
|
||||
if (stages.empty() || weight <= character->weight)
|
||||
return 0;
|
||||
for (int i = 0; i < (int)stages.size(); i++)
|
||||
if (weight < stages[i].weight)
|
||||
return i;
|
||||
return (int)stages.size();
|
||||
}
|
||||
|
||||
float Play::stage_threshold_get(int index)
|
||||
{
|
||||
std::vector<Stage>& stages = character->stages;
|
||||
if (index <= 0 || stages.empty())
|
||||
return character->weight;
|
||||
return stages[std::min(index, (int)stages.size()) - 1].weight;
|
||||
}
|
||||
|
||||
float Play::stage_progress_get()
|
||||
{
|
||||
float low = stage_threshold_get(stage);
|
||||
float high = stage_threshold_get(stage + 1);
|
||||
if (stage >= (int)character->stages.size() || high <= low)
|
||||
return 1;
|
||||
return (progress.weight - low) / (high - low);
|
||||
}
|
||||
|
||||
std::string Play::suffix_get()
|
||||
{
|
||||
return stage > 0 && stage <= (int)character->stages.size() ? character->stages[stage - 1].animationSuffix : "";
|
||||
}
|
||||
|
||||
std::string Play::idle_get()
|
||||
{
|
||||
if (isStageUp)
|
||||
return "";
|
||||
if (!idleOverride.empty())
|
||||
return idleOverride;
|
||||
return is_over_capacity() && !character->animationIdleFull.empty() ? character->animationIdleFull
|
||||
: character->animationIdle;
|
||||
}
|
||||
|
||||
// Plays one of the character's animations, as its current stage draws it.
|
||||
void Play::body_play(const std::string& animation, Transition transition, Interrupt interrupt)
|
||||
{
|
||||
if (!animation.empty())
|
||||
actor.play(animation + suffix_get(), transition, interrupt);
|
||||
}
|
||||
|
||||
void Play::idle_play()
|
||||
{
|
||||
body_play(idle_get());
|
||||
}
|
||||
|
||||
void Play::idle_override_set(const std::string& animation)
|
||||
{
|
||||
if (!animation.empty())
|
||||
idleOverride = animation == character->animationIdle ? "" : animation;
|
||||
}
|
||||
|
||||
void Play::values_clamp()
|
||||
{
|
||||
Character& data = *character;
|
||||
progress.weight = value_clamp(progress.weight, data.weight, data.weightMax);
|
||||
progress.capacity = value_clamp(progress.capacity, data.capacityMin, data.capacityMax);
|
||||
progress.digestionRate = value_clamp(progress.digestionRate, data.digestionRateMin, data.digestionRateMax);
|
||||
progress.calories = value_clamp(progress.calories, 0, std::max(0.0f, capacity_max_get()));
|
||||
progress.digestionProgress = value_clamp(progress.digestionProgress, 0, DIGESTION_MAX);
|
||||
progress.digestionTimer = std::clamp(progress.digestionTimer, 0, std::max(0, data.digestionTime));
|
||||
progress.totalCalories = std::max(0.0f, progress.totalCalories);
|
||||
progress.playtime = std::max(0.0f, progress.playtime);
|
||||
}
|
||||
|
||||
// Swaps a layer's region for the one a source layer shows at a time, while active. Turns itself off (and returns
|
||||
// false) when the animation lacks the source or the time runs past it.
|
||||
static bool is_region_swapped(Actor& actor, int source, float& time, Override& swap, bool isActive)
|
||||
{
|
||||
Anm2Animation* animation = actor.animation_get();
|
||||
swap.isRegion = false;
|
||||
if (!isActive || !animation)
|
||||
return false;
|
||||
for (Anm2Item& layer : animation->layers)
|
||||
{
|
||||
if (layer.id != source || layer.frames.empty())
|
||||
continue;
|
||||
Anm2Frame frame = actor.frame_get(layer.frames, time, TARGET_LAYER, source);
|
||||
swap = {.crop = frame.crop, .pivot = frame.pivot, .size = frame.size, .id = swap.id, .isRegion = true};
|
||||
time += TALK_STEP;
|
||||
return true;
|
||||
}
|
||||
time = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
static int source_length_get(Actor& actor, int source)
|
||||
{
|
||||
Anm2Animation* animation = actor.animation_get();
|
||||
if (!animation)
|
||||
return -1;
|
||||
for (Anm2Item& layer : animation->layers)
|
||||
if (layer.id == source)
|
||||
return track_length_get(layer.frames);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void Play::body_update()
|
||||
{
|
||||
Character& data = *character;
|
||||
values_clamp();
|
||||
isJustStageUp = false;
|
||||
isJustStageFinal = false;
|
||||
isJustDigested = false;
|
||||
if (!actor.isPlaying)
|
||||
{
|
||||
if (isStageUp)
|
||||
{
|
||||
(stage >= (int)data.stages.size() ? isJustStageFinal : isJustStageUp) = true;
|
||||
isStageUp = false;
|
||||
actor.isLocked = false;
|
||||
}
|
||||
if (!isTalking)
|
||||
idle_play();
|
||||
}
|
||||
actor.update(*audio);
|
||||
|
||||
if (progress.isDigesting && --progress.digestionTimer <= 0)
|
||||
{
|
||||
float nextWeight = value_clamp(progress.weight + progress.calories * data.caloriesToKilogram, data.weight,
|
||||
data.weightMax);
|
||||
if (is_over_capacity())
|
||||
{
|
||||
float over = progress.calories - progress.capacity;
|
||||
float overPercent = over / (capacity_max_get() - progress.capacity);
|
||||
progress.capacity = value_clamp(progress.capacity + overPercent * data.overstuffedDigestBonus * over,
|
||||
data.capacityMin, data.capacityMax);
|
||||
}
|
||||
progress.calories = 0;
|
||||
if (int nextStage = stage_get(nextWeight); nextStage > stage_get(progress.weight))
|
||||
{
|
||||
actor.interrupt = INTERRUPT_IF_ALLOWED;
|
||||
body_play(data.animationStageUp, TRANSITION_NORMAL, INTERRUPT_NEVER);
|
||||
stage = nextStage;
|
||||
isStageUp = true;
|
||||
actor.isLocked = true;
|
||||
}
|
||||
else
|
||||
isJustDigested = true;
|
||||
progress.weight = nextWeight;
|
||||
progress.isDigesting = false;
|
||||
progress.digestionTimer = data.digestionTime;
|
||||
progress.digestionProgress = 0;
|
||||
}
|
||||
else if (!progress.isDigesting)
|
||||
{
|
||||
if (progress.calories > 0)
|
||||
progress.digestionProgress += progress.digestionRate;
|
||||
if (progress.digestionProgress >= DIGESTION_MAX)
|
||||
{
|
||||
progress.isDigesting = true;
|
||||
progress.digestionTimer = data.digestionTime;
|
||||
audio->play(data.sounds[SOUND_DIGEST]);
|
||||
}
|
||||
}
|
||||
values_clamp();
|
||||
if (is_chance(data.gurgleChance * capacity_percent_get() * data.gurgleCapacityMultiplier * PERCENT))
|
||||
audio->play(data.sounds[SOUND_GURGLE]);
|
||||
stage = stage_get(progress.weight);
|
||||
|
||||
float stageProgress = stage_progress_get();
|
||||
float capacityProgress = capacity_percent_get();
|
||||
if (progress.isDigesting)
|
||||
capacityProgress *= (float)progress.digestionTimer / std::max(1, data.digestionTime);
|
||||
for (int i = 0; i < (int)data.expandAreas.size(); i++)
|
||||
{
|
||||
float scaleMax = data.expandAreas[i].scaleMax;
|
||||
float add = std::clamp((stageProgress + capacityProgress) * scaleMax / 2, 0.0f, scaleMax);
|
||||
actor.overrides[OVERRIDE_EXPAND + i * 2].scale = {add, add};
|
||||
actor.overrides[OVERRIDE_EXPAND + i * 2 + 1].scale = {add, add};
|
||||
}
|
||||
|
||||
isTalking = is_region_swapped(actor, data.talkSource, talkTime, actor.overrides[OVERRIDE_TALK], isTalking);
|
||||
if (talkTime > source_length_get(actor, data.talkSource))
|
||||
talkTime = 0;
|
||||
int blinkLength = source_length_get(actor, data.blinkSource);
|
||||
if (blinkLength != 1)
|
||||
{
|
||||
isBlinking |= is_chance(data.blinkChance);
|
||||
isBlinking = is_region_swapped(actor, data.blinkSource, blinkTime, actor.overrides[OVERRIDE_BLINK], isBlinking);
|
||||
if (blinkTime >= blinkLength)
|
||||
{
|
||||
isBlinking = false;
|
||||
blinkTime = 0;
|
||||
actor.overrides[OVERRIDE_BLINK].isRegion = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Play::camera_update(Vec2 screenSize)
|
||||
// Pans with the middle mouse (or control and the left), zooms through the ladder with the wheel toward the mouse, and
|
||||
// settles the screen shake.
|
||||
void Camera::update(Play& play, Vec2 screenSize)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
Cursor& cursor = play.character->cursor;
|
||||
if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) && !ImGui::IsAnyItemActive())
|
||||
{
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Middle) ||
|
||||
(ImGui::IsMouseDown(ImGuiMouseButton_Left) && ImGui::IsKeyDown(ImGuiMod_Ctrl)))
|
||||
{
|
||||
cursor.play(character->cursorPan);
|
||||
play.cursor.play(cursor.pan);
|
||||
pan = pan - Vec2{io.MouseDelta.x, io.MouseDelta.y} / (zoom / PERCENT);
|
||||
}
|
||||
if (io.MouseWheel != 0)
|
||||
{
|
||||
Vec2 mouse = {io.MousePos.x, io.MousePos.y};
|
||||
Vec2 world = world_get(mouse);
|
||||
cursor.play(character->cursorZoom);
|
||||
play.cursor.play(cursor.zoom);
|
||||
int index = std::clamp(zoom_index_get(zoom) + (io.MouseWheel > 0 ? 1 : -1), 0,
|
||||
(int)std::size(ZOOM_LADDER) - 1);
|
||||
zoom = ZOOM_LADDER[index];
|
||||
@@ -413,7 +350,7 @@ void Play::camera_update(Vec2 screenSize)
|
||||
}
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Home))
|
||||
focus(screenSize);
|
||||
play.focus(screenSize);
|
||||
|
||||
if (shakeTime > 0)
|
||||
{
|
||||
@@ -430,152 +367,3 @@ void Play::camera_update(Vec2 screenSize)
|
||||
shake = {};
|
||||
}
|
||||
}
|
||||
|
||||
// The Konami code unlocks the cheats menu, keyed within a few seconds of each press.
|
||||
void Play::cheats_update()
|
||||
{
|
||||
if (isCheats)
|
||||
return;
|
||||
for (ImGuiKey key : {ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_B,
|
||||
ImGuiKey_A})
|
||||
{
|
||||
if (!ImGui::IsKeyPressed(key, false))
|
||||
continue;
|
||||
cheatIndex = key == CHEAT_CODE[cheatIndex] ? cheatIndex + 1 : key == CHEAT_CODE[0] ? 1 : 0;
|
||||
cheatTime = (float)ImGui::GetTime();
|
||||
if (cheatIndex < (int)std::size(CHEAT_CODE))
|
||||
continue;
|
||||
isCheats = true;
|
||||
cheatIndex = 0;
|
||||
toast_add(STRING_ToastCheatsUnlocked);
|
||||
audio->play(character->sounds[SOUND_CHEATS]);
|
||||
}
|
||||
if (cheatIndex > 0 && ImGui::GetTime() - cheatTime > CHEAT_TIME)
|
||||
cheatIndex = 0;
|
||||
}
|
||||
|
||||
void Play::particles_spawn(const std::vector<std::string>& names, Vec2 position)
|
||||
{
|
||||
for (const std::string& name : names)
|
||||
{
|
||||
auto found = character->particles.find(name);
|
||||
if (found == character->particles.end() || !found->second.anm2)
|
||||
SDL_Log("Particle not found: %s", name.c_str());
|
||||
else
|
||||
particles.emplace_back(found->second.anm2, position, found->second.animation);
|
||||
}
|
||||
}
|
||||
|
||||
void Play::update(Vec2 screenSize)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (std::exchange(isFocusPending, false))
|
||||
{
|
||||
focus(screenSize);
|
||||
pan = progress.zoom >= ZOOM_LADDER[0] ? progress.pan : pan;
|
||||
zoom = progress.zoom >= ZOOM_LADDER[0] ? ZOOM_LADDER[zoom_index_get(progress.zoom)] : zoom;
|
||||
}
|
||||
progress.playtime += io.DeltaTime;
|
||||
cheats_update();
|
||||
text.update(*this, screenSize);
|
||||
|
||||
if (isJustStageFinal && !isPostgame && !isPostgamePending)
|
||||
{
|
||||
Entry* end = character->dialogue.hook_get("End");
|
||||
isPostgame = isCheats = !end;
|
||||
if (end)
|
||||
{
|
||||
text.set(*this, end, TEXT_UNINTERRUPTIBLE);
|
||||
isMenuOpen = isToolsOpen = false;
|
||||
progress.calories = 0;
|
||||
progress.digestionProgress = 0;
|
||||
item_drop();
|
||||
isPostgamePending = true;
|
||||
focus(screenSize);
|
||||
}
|
||||
}
|
||||
if (isPostgamePending && (!text.entry || (text.entry->isTerminal && text.isFinished)) && !text.isScene)
|
||||
{
|
||||
isMenuOpen = isPostgame = isCheats = true;
|
||||
isPostgamePending = false;
|
||||
focus(screenSize);
|
||||
}
|
||||
|
||||
if (isAutofeed)
|
||||
for (int i = 0; i < (int)character->interactTypes.size(); i++)
|
||||
if (character->interactTypes[i].name == AUTOFEED_TYPE)
|
||||
cursorType = i;
|
||||
Pointer pointer;
|
||||
pointer.position = world_get({io.MousePos.x, io.MousePos.y});
|
||||
pointer.type = cursorType;
|
||||
pointer.isCaptured = io.WantCaptureMouse;
|
||||
pointer.isClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
|
||||
pointer.isDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
|
||||
pointer.isReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
||||
pointer.isRightClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
|
||||
Pointer itemPointer = pointer;
|
||||
Pointer interactPointer = pointer;
|
||||
if (text.isScene)
|
||||
{
|
||||
itemPointer = interactPointer = {.position = pointer.position, .isCaptured = true};
|
||||
isAutofeedActive = false;
|
||||
}
|
||||
else
|
||||
autofeed_update(itemPointer, interactPointer);
|
||||
items_update(itemPointer);
|
||||
interact_update(interactPointer);
|
||||
|
||||
if (character->cursor && !isAutofeedActive && pointer.isClicked && !isItemHovered && !isHovering &&
|
||||
cursorState == CURSOR_DEFAULT)
|
||||
cursor.play(character->cursorClick, TRANSITION_FORCE, INTERRUPT_ALWAYS);
|
||||
|
||||
body_update();
|
||||
int areaIndex = character->area_get(stage);
|
||||
if (areaIndex >= 0 && area.anm2 != character->areas[areaIndex].anm2)
|
||||
area = Actor(character->areas[areaIndex].anm2);
|
||||
if (areaIndex >= 0 && is_rect_valid(character->areas[areaIndex].bounds))
|
||||
bounds = character->areas[areaIndex].bounds;
|
||||
area.update(*audio);
|
||||
|
||||
for (int i = 0; i < (int)particles.size(); i++)
|
||||
{
|
||||
particles[i].update(*audio);
|
||||
if (!particles[i].isPlaying)
|
||||
particles.erase(particles.begin() + i--);
|
||||
}
|
||||
for (Actor* each : {&cursor, &autoCursor})
|
||||
{
|
||||
bool isActive = (each == &autoCursor) == isAutofeedActive;
|
||||
if ((!isActive || cursorState == CURSOR_DEFAULT) && !each->pending)
|
||||
each->play(character->cursorIdle, TRANSITION_IF_IDLE);
|
||||
each->update(*audio);
|
||||
}
|
||||
cursorState = CURSOR_DEFAULT;
|
||||
cursor.position = {io.MousePos.x, io.MousePos.y};
|
||||
cursor.isVisible = true;
|
||||
camera_update(screenSize);
|
||||
|
||||
for (int i = 0; i < (int)toasts.size(); i++)
|
||||
if (--toasts[i].life <= 0)
|
||||
toasts.erase(toasts.begin() + i--);
|
||||
if ((autosaveTime += io.DeltaTime) > AUTOSAVE_TIME)
|
||||
{
|
||||
save();
|
||||
autosaveTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Play::render(Renderer& renderer, Vec2 screenSize)
|
||||
{
|
||||
float scale = zoom / PERCENT;
|
||||
Affine view = {scale, 0, 0, scale, -pan.x * scale + shake.x * screenSize.x, -pan.y * scale + shake.y * screenSize.y};
|
||||
area.render(renderer, view);
|
||||
actor.render(renderer, view);
|
||||
if (!text.isScene)
|
||||
for (ItemEntity& entity : items)
|
||||
entity.actor.render(renderer, view);
|
||||
if (isAutofeedActive)
|
||||
autoCursor.render(renderer, view);
|
||||
for (Actor& particle : particles)
|
||||
particle.render(renderer, view);
|
||||
}
|
||||
+159
-94
@@ -5,6 +5,7 @@
|
||||
#include "save.h"
|
||||
|
||||
constexpr float DIGESTION_MAX = 100;
|
||||
constexpr float TICK = 1 / TICK_RATE;
|
||||
constexpr int TOAST_LIFE = 180;
|
||||
constexpr float ZOOM_LADDER[] = {100, 115, 130, 150, 175, 200, 235, 275, 325, 400};
|
||||
|
||||
@@ -14,12 +15,6 @@ enum Game
|
||||
GAME_CONTINUE
|
||||
};
|
||||
|
||||
enum Hold
|
||||
{
|
||||
HOLD_RELEASE,
|
||||
HOLD_KEEP
|
||||
};
|
||||
|
||||
enum CursorState
|
||||
{
|
||||
CURSOR_DEFAULT,
|
||||
@@ -36,18 +31,32 @@ enum Focus
|
||||
FOCUS_BOTH
|
||||
};
|
||||
|
||||
enum Hold
|
||||
{
|
||||
HOLD_RELEASE,
|
||||
HOLD_KEEP
|
||||
};
|
||||
|
||||
// What happened to the body in a tick, answered once it's done.
|
||||
enum Event
|
||||
{
|
||||
EVENT_DIGESTED,
|
||||
EVENT_STAGE_UP,
|
||||
EVENT_STAGE_FINAL
|
||||
};
|
||||
|
||||
// The mouse as items and interact areas see it this tick: the player's, or the autofeed's stand-in.
|
||||
struct Pointer
|
||||
{
|
||||
Vec2 position;
|
||||
int target = -1;
|
||||
int type = -1;
|
||||
bool isAutomated = false;
|
||||
bool isCaptured = false;
|
||||
bool isClicked = false;
|
||||
bool isDown = false;
|
||||
bool isReleased = false;
|
||||
bool isRightClicked = false;
|
||||
bool isAutomated = false;
|
||||
};
|
||||
|
||||
struct ItemEntity
|
||||
@@ -56,11 +65,6 @@ struct ItemEntity
|
||||
Vec2 velocity;
|
||||
float angularVelocity = 0;
|
||||
int id = 0;
|
||||
|
||||
float& rotation_get()
|
||||
{
|
||||
return actor.overrides[0].rotation;
|
||||
}
|
||||
};
|
||||
|
||||
struct Toast
|
||||
@@ -69,120 +73,181 @@ struct Toast
|
||||
int life = 0;
|
||||
};
|
||||
|
||||
class Renderer;
|
||||
class Audio;
|
||||
struct Settings;
|
||||
class Play;
|
||||
|
||||
// A character being played: its body (weight, digestion, stages), the items in the world, what the cursor does to
|
||||
// both, the autofeed, the camera and the effects.
|
||||
// The character's body: its stats, digestion and stages, and the animations they choose.
|
||||
class Body
|
||||
{
|
||||
public:
|
||||
Stats stats;
|
||||
Actor actor;
|
||||
std::string idleOverride;
|
||||
std::string suffix;
|
||||
Character* character = nullptr;
|
||||
Audio* audio = nullptr;
|
||||
float blinkTime = 0;
|
||||
float talkTime = 0;
|
||||
int stage = 0;
|
||||
bool isBlinking = false;
|
||||
bool isStageUp = false;
|
||||
bool isTalking = false;
|
||||
|
||||
void set(Character& played, Audio& sound, Game game);
|
||||
void update(std::vector<Event>& events);
|
||||
void play(const std::string& animation, Transition transition = TRANSITION_NORMAL,
|
||||
Interrupt interrupt = INTERRUPT_IF_ALLOWED);
|
||||
void idle_play();
|
||||
void idle_override_set(const std::string& animation);
|
||||
void stage_set(int index);
|
||||
void values_clamp();
|
||||
std::string idle_get();
|
||||
Rect eat_rect_get(const EatArea& eatArea);
|
||||
float capacity_max_get();
|
||||
float capacity_percent_get();
|
||||
float stage_progress_get();
|
||||
float stage_threshold_get(int index);
|
||||
int stage_get(float weight);
|
||||
bool is_over_capacity();
|
||||
};
|
||||
|
||||
// Food being carried to an eat area, until the area's event fires.
|
||||
struct Feeding
|
||||
{
|
||||
Rect rect = {NAN, NAN, NAN, NAN};
|
||||
int animation = -1;
|
||||
int event = -1;
|
||||
int item = -1;
|
||||
bool isTextFull = false;
|
||||
bool isTextPending = false;
|
||||
};
|
||||
|
||||
// The items in the world: spawning them, their physics, and carrying them to be eaten or used.
|
||||
class Items
|
||||
{
|
||||
public:
|
||||
std::vector<ItemEntity> list;
|
||||
std::vector<int> spawnQueue;
|
||||
std::vector<bool> discovered;
|
||||
Feeding feeding;
|
||||
Vec2 pointerPrevious;
|
||||
int held = -1;
|
||||
int spawnDelay = 0;
|
||||
int tooltip = -1;
|
||||
bool isHovered = false;
|
||||
bool isJustHeld = false;
|
||||
|
||||
void update(Play& play, Pointer pointer);
|
||||
void add(Play& play, int id, Vec2 position, Vec2 velocity = {}, float rotation = 0);
|
||||
void drop(Hold hold = HOLD_RELEASE);
|
||||
void spawn_queue_add(Play& play, int id);
|
||||
void spawn_queue_add(Play& play);
|
||||
int spawn_delay_get(Play& play);
|
||||
bool is_spawn_possible(Play& play);
|
||||
};
|
||||
|
||||
// The cursor on the character's interact areas: rubbing, smacking and the like.
|
||||
class Interactions
|
||||
{
|
||||
public:
|
||||
std::vector<int> cooldowns;
|
||||
Vec2 pointerPrevious;
|
||||
int area = -1;
|
||||
bool isHolding = false;
|
||||
bool isHovering = false;
|
||||
bool isInteracting = false;
|
||||
|
||||
void update(Play& play, Pointer pointer);
|
||||
};
|
||||
|
||||
// Plays for the player: spawns food, carries it to the mouth, and rubs or pats the belly along when nothing fits.
|
||||
class Autofeed
|
||||
{
|
||||
public:
|
||||
Vec2 velocity;
|
||||
float angle = 0;
|
||||
int clickCooldown = 0;
|
||||
int target = -1;
|
||||
bool isActive = false;
|
||||
bool isDigestHeld = false;
|
||||
bool isEnabled = false;
|
||||
bool isItemHeld = false;
|
||||
|
||||
void update(Play& play, Pointer& itemPointer, Pointer& interactPointer);
|
||||
int type_get(Play& play);
|
||||
bool is_available(Play& play);
|
||||
|
||||
private:
|
||||
void reset();
|
||||
};
|
||||
|
||||
class Camera
|
||||
{
|
||||
public:
|
||||
Vec2 pan;
|
||||
Vec2 shake;
|
||||
float zoom = ZOOM_LADDER[0];
|
||||
int shakeTime = 0;
|
||||
int shakeTimeMax = 0;
|
||||
bool isFocusPending = false;
|
||||
|
||||
void update(Play& play, Vec2 screenSize);
|
||||
void focus(Rect target, Vec2 screenSize, Focus panels);
|
||||
void shake_add(float magnitude, int time);
|
||||
Vec2 world_get(Vec2 screen);
|
||||
Affine view_get(Vec2 screenSize);
|
||||
};
|
||||
|
||||
// A character being played, sequencing its parts each tick: the body, items, interactions, autofeed, camera and
|
||||
// dialogue.
|
||||
class Play
|
||||
{
|
||||
public:
|
||||
Character* character = nullptr;
|
||||
Audio* audio = nullptr;
|
||||
Progress progress;
|
||||
Autofeed autofeed;
|
||||
Body body;
|
||||
Camera camera;
|
||||
Interactions interactions;
|
||||
Items items;
|
||||
Text text;
|
||||
Actor actor;
|
||||
Actor area;
|
||||
Actor autoCursor;
|
||||
Actor cursor;
|
||||
std::string idleOverride;
|
||||
std::vector<Actor> particles;
|
||||
std::vector<ItemEntity> items;
|
||||
std::vector<Event> events;
|
||||
std::vector<Toast> toasts;
|
||||
std::vector<int> dialogueCooldowns;
|
||||
std::vector<int> spawnQueue;
|
||||
std::vector<bool> discovered;
|
||||
Rect bounds;
|
||||
Vec2 autoVelocity;
|
||||
Vec2 itemPointerPrevious;
|
||||
Vec2 pan;
|
||||
Vec2 pointerPrevious;
|
||||
Vec2 shake;
|
||||
float autoAngle = 0;
|
||||
float autosaveTime = 0;
|
||||
float blinkTime = 0;
|
||||
float cheatTime = 0;
|
||||
float talkTime = 0;
|
||||
float zoom = ZOOM_LADDER[0];
|
||||
int autoClickCooldown = 0;
|
||||
int autoTarget = -1;
|
||||
int cheatIndex = 0;
|
||||
int cursorType = -1;
|
||||
int heldItem = -1;
|
||||
int interactArea = -1;
|
||||
int eatAnimation = -1;
|
||||
int eatEvent = -1;
|
||||
int eatItem = -1;
|
||||
int shakeTime = 0;
|
||||
int shakeTimeMax = 0;
|
||||
int spawnDelay = 0;
|
||||
int stage = 0;
|
||||
int tooltipItem = -1;
|
||||
CursorState cursorState = CURSOR_DEFAULT;
|
||||
Rect eatRect = {NAN, NAN, NAN, NAN};
|
||||
bool isAutofeed = false;
|
||||
bool isAutofeedActive = false;
|
||||
bool isAutoDigestHeld = false;
|
||||
bool isAutoItemHeld = false;
|
||||
bool isBlinking = false;
|
||||
bool isCheats = false;
|
||||
bool isEatTextFull = false;
|
||||
bool isEatTextPending = false;
|
||||
bool isFocusPending = false;
|
||||
bool isHoldInteracting = false;
|
||||
bool isHovering = false;
|
||||
bool isInteracting = false;
|
||||
bool isItemHovered = false;
|
||||
bool isJustItemHeld = false;
|
||||
bool isJustDigested = false;
|
||||
bool isJustStageFinal = false;
|
||||
bool isJustStageUp = false;
|
||||
bool isMenuOpen = true;
|
||||
bool isPostgame = false;
|
||||
bool isPostgamePending = false;
|
||||
bool isStageUp = false;
|
||||
bool isTalking = false;
|
||||
bool isToolsOpen = false;
|
||||
|
||||
void set(Character& played, Game game, Renderer& renderer, Audio& sound);
|
||||
void save();
|
||||
void update(Vec2 screenSize);
|
||||
void render(Renderer& renderer, Vec2 screenSize);
|
||||
void toast_add(StringId message);
|
||||
void shake_add(float magnitude, int time);
|
||||
void focus(Vec2 screenSize);
|
||||
Vec2 world_get(Vec2 screen);
|
||||
void toast_add(const std::string& message);
|
||||
void particles_spawn(const std::vector<std::string>& names, Vec2 position);
|
||||
Actor& pointer_actor_get();
|
||||
Focus focus_get();
|
||||
|
||||
float capacity_max_get();
|
||||
float capacity_percent_get();
|
||||
bool is_over_capacity();
|
||||
int stage_get(float weight);
|
||||
float stage_progress_get();
|
||||
float stage_threshold_get(int index);
|
||||
std::string idle_get();
|
||||
std::string suffix_get();
|
||||
void body_play(const std::string& animation, Transition transition = TRANSITION_NORMAL,
|
||||
Interrupt interrupt = INTERRUPT_IF_ALLOWED);
|
||||
void idle_play();
|
||||
void idle_override_set(const std::string& animation);
|
||||
void values_clamp();
|
||||
|
||||
void item_add(int id, Vec2 position, Vec2 velocity = {}, float rotation = 0);
|
||||
int spawn_delay_get();
|
||||
bool is_spawn_possible();
|
||||
void spawn_queue_add(int id);
|
||||
void spawn_queue_add();
|
||||
void particles_spawn(const std::vector<std::string>& names, Vec2 position);
|
||||
Rect eat_rect_get(const EatArea& eatArea);
|
||||
template <class Visitor> void visit(Visitor&& visitor)
|
||||
{
|
||||
visitor("pan", camera.pan);
|
||||
visitor("postgame", isPostgame);
|
||||
visitor("spawn_delay", items.spawnDelay);
|
||||
visitor("zoom", camera.zoom);
|
||||
}
|
||||
|
||||
private:
|
||||
void body_update();
|
||||
void camera_update(Vec2 screenSize);
|
||||
void area_set();
|
||||
void cheats_update();
|
||||
void interact_update(Pointer pointer);
|
||||
void items_update(Pointer pointer);
|
||||
void autofeed_update(Pointer& itemPointer, Pointer& interactPointer);
|
||||
void item_drop(Hold hold = HOLD_RELEASE);
|
||||
void events_handle(Vec2 screenSize);
|
||||
};
|
||||
@@ -35,6 +35,8 @@ class Renderer
|
||||
{
|
||||
public:
|
||||
Texture white;
|
||||
bool isShowLayers = false;
|
||||
bool isShowNulls = false;
|
||||
bool isValid = false;
|
||||
|
||||
void init();
|
||||
|
||||
+2
-174
@@ -1,96 +1,13 @@
|
||||
#include "save.h"
|
||||
#include <SDL3/SDL.h>
|
||||
#include <cstdio>
|
||||
#include <type_traits>
|
||||
|
||||
#ifdef SDL_PLATFORM_EMSCRIPTEN
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
|
||||
constexpr const char* KEY_DISCOVERED = "discovered";
|
||||
constexpr const char* KEY_ITEM = "item";
|
||||
constexpr const char* SETTINGS_FILE = "settings.txt";
|
||||
|
||||
static void value_read(const char* text, float& value)
|
||||
{
|
||||
value = (float)SDL_atof(text);
|
||||
}
|
||||
|
||||
static void value_read(const char* text, int& value)
|
||||
{
|
||||
value = SDL_atoi(text);
|
||||
}
|
||||
|
||||
static void value_read(const char* text, bool& value)
|
||||
{
|
||||
value = SDL_atoi(text);
|
||||
}
|
||||
|
||||
static void value_read(const char* text, Measurement& value)
|
||||
{
|
||||
value = (Measurement)SDL_atoi(text);
|
||||
}
|
||||
|
||||
static void value_read(const char* text, std::string& value)
|
||||
{
|
||||
value = text;
|
||||
}
|
||||
|
||||
static void value_read(const char* text, Vec2& value)
|
||||
{
|
||||
SDL_sscanf(text, "%f %f", &value.x, &value.y);
|
||||
}
|
||||
|
||||
static void value_read(const char* text, Color& value)
|
||||
{
|
||||
SDL_sscanf(text, "%f %f %f", &value.r, &value.g, &value.b);
|
||||
}
|
||||
|
||||
static std::string value_write(float value)
|
||||
{
|
||||
return std::to_string(value);
|
||||
}
|
||||
|
||||
static std::string value_write(int value)
|
||||
{
|
||||
return std::to_string(value);
|
||||
}
|
||||
|
||||
static std::string value_write(const std::string& value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
static std::string value_write(Vec2 value)
|
||||
{
|
||||
return value_write(value.x) + " " + value_write(value.y);
|
||||
}
|
||||
|
||||
static std::string value_write(Color value)
|
||||
{
|
||||
return value_write(value.r) + " " + value_write(value.g) + " " + value_write(value.b);
|
||||
}
|
||||
|
||||
// Calls line with each key and value of a key=value text file. Returns whether it was there.
|
||||
template <class Line> static bool lines_read(const std::string& path, Line&& line)
|
||||
{
|
||||
char* text = (char*)SDL_LoadFile(path.c_str(), nullptr);
|
||||
if (!text)
|
||||
return false;
|
||||
char* rest = nullptr;
|
||||
for (char* entry = SDL_strtok_r(text, "\r\n", &rest); entry; entry = SDL_strtok_r(nullptr, "\r\n", &rest))
|
||||
{
|
||||
char* value = SDL_strchr(entry, '=');
|
||||
if (!value)
|
||||
continue;
|
||||
*value++ = '\0';
|
||||
line(std::string(entry), value);
|
||||
}
|
||||
SDL_free(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void file_write(const std::string& path, const std::string& text)
|
||||
void file_write(const std::string& path, const std::string& text)
|
||||
{
|
||||
SDL_CreateDirectory(path.substr(0, path.find_last_of('/')).c_str());
|
||||
if (!SDL_SaveFile(path.c_str(), text.data(), text.size()))
|
||||
@@ -100,48 +17,9 @@ static void file_write(const std::string& path, const std::string& text)
|
||||
#endif
|
||||
}
|
||||
|
||||
// Reads every field a visitable struct names from a key=value file, and writes them back.
|
||||
template <class Fields> static bool fields_read(const std::string& path, Fields& fields)
|
||||
{
|
||||
return lines_read(path, [&](const std::string& key, const char* value) {
|
||||
fields.visit([&](const char* name, auto& field) {
|
||||
if (key == name)
|
||||
value_read(value, field);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
template <class Fields> static std::string fields_write(Fields& fields)
|
||||
{
|
||||
std::string text;
|
||||
fields.visit([&](const char* name, auto& field) {
|
||||
using Type = std::decay_t<decltype(field)>;
|
||||
if constexpr (std::is_same_v<Type, bool> || std::is_enum_v<Type>)
|
||||
text += std::string(name) + "=" + value_write((int)field) + "\n";
|
||||
else
|
||||
text += std::string(name) + "=" + value_write(field) + "\n";
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
template <class Visitor> void Settings::visit(Visitor&& visitor)
|
||||
{
|
||||
visitor("character_color", isCharacterColor);
|
||||
visitor("color", color);
|
||||
visitor("console", isConsole);
|
||||
visitor("height", height);
|
||||
visitor("last_character", lastCharacter);
|
||||
visitor("measurement", measurement);
|
||||
visitor("mute", isMuted);
|
||||
visitor("volume", volume);
|
||||
visitor("width", width);
|
||||
visitor("x", x);
|
||||
visitor("y", y);
|
||||
}
|
||||
|
||||
void Settings::load()
|
||||
{
|
||||
fields_read(pref_path_get(SETTINGS_FILE), *this);
|
||||
is_fields_read(pref_path_get(SETTINGS_FILE), *this);
|
||||
}
|
||||
|
||||
void Settings::save()
|
||||
@@ -159,53 +37,3 @@ void Settings::reset()
|
||||
isCharacterColor = defaults.isCharacterColor;
|
||||
isMuted = defaults.isMuted;
|
||||
}
|
||||
|
||||
template <class Visitor> void Progress::visit(Visitor&& visitor)
|
||||
{
|
||||
visitor("alternate", isAlternate);
|
||||
visitor("calories", calories);
|
||||
visitor("capacity", capacity);
|
||||
visitor("digesting", isDigesting);
|
||||
visitor("digestion_progress", digestionProgress);
|
||||
visitor("digestion_rate", digestionRate);
|
||||
visitor("digestion_timer", digestionTimer);
|
||||
visitor("pan", pan);
|
||||
visitor("playtime", playtime);
|
||||
visitor("postgame", isPostgame);
|
||||
visitor("spawn_delay", spawnDelay);
|
||||
visitor("total_calories", totalCalories);
|
||||
visitor("total_consumed", totalConsumed);
|
||||
visitor("total_spawned", totalSpawned);
|
||||
visitor("weight", weight);
|
||||
visitor("zoom", zoom);
|
||||
}
|
||||
|
||||
// Items are kept one per line as their position, velocity, rotation and then name.
|
||||
void Progress::load(const std::string& path)
|
||||
{
|
||||
*this = {};
|
||||
isValid = fields_read(path, *this);
|
||||
lines_read(path, [&](const std::string& key, const char* value) {
|
||||
if (key == KEY_DISCOVERED)
|
||||
discovered.emplace_back(value);
|
||||
SavedItem item;
|
||||
int length = 0;
|
||||
if (key == KEY_ITEM && std::sscanf(value, "%f %f %f %f %f %n", &item.position.x, &item.position.y,
|
||||
&item.velocity.x, &item.velocity.y, &item.rotation, &length) == 5)
|
||||
{
|
||||
item.name = value + length;
|
||||
items.push_back(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void Progress::save(const std::string& path)
|
||||
{
|
||||
std::string text = fields_write(*this);
|
||||
for (const std::string& name : discovered)
|
||||
text += std::string(KEY_DISCOVERED) + "=" + name + "\n";
|
||||
for (const SavedItem& item : items)
|
||||
text += std::string(KEY_ITEM) + "=" + value_write(item.position) + " " + value_write(item.velocity) + " " +
|
||||
value_write(item.rotation) + " " + item.name + "\n";
|
||||
file_write(path, text);
|
||||
}
|
||||
+108
-22
@@ -1,9 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "common.h"
|
||||
#include <SDL3/SDL_iostream.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <type_traits>
|
||||
|
||||
// Text files of key=value lines. A struct names the fields it keeps with visit(visitor), calling visitor(key, field)
|
||||
// for each, and reads and writes them through fields_read and fields_write.
|
||||
|
||||
enum Measurement
|
||||
{
|
||||
@@ -11,6 +16,76 @@ enum Measurement
|
||||
MEASUREMENT_IMPERIAL
|
||||
};
|
||||
|
||||
void file_write(const std::string& path, const std::string& text);
|
||||
|
||||
// Calls line with each key and value of a file. Returns whether it was there.
|
||||
template <class Line> bool is_file_read(const std::string& path, Line&& line)
|
||||
{
|
||||
char* text = (char*)SDL_LoadFile(path.c_str(), nullptr);
|
||||
if (!text)
|
||||
return false;
|
||||
char* rest = nullptr;
|
||||
for (char* entry = SDL_strtok_r(text, "\r\n", &rest); entry; entry = SDL_strtok_r(nullptr, "\r\n", &rest))
|
||||
{
|
||||
char* value = SDL_strchr(entry, '=');
|
||||
if (!value)
|
||||
continue;
|
||||
*value++ = '\0';
|
||||
line(std::string(entry), (const char*)value);
|
||||
}
|
||||
SDL_free(text);
|
||||
return true;
|
||||
}
|
||||
|
||||
template <class Field> void field_read(const char* text, Field& field)
|
||||
{
|
||||
if constexpr (std::is_same_v<Field, std::string>)
|
||||
field = text;
|
||||
else if constexpr (std::is_same_v<Field, Vec2>)
|
||||
std::sscanf(text, "%f %f", &field.x, &field.y);
|
||||
else if constexpr (std::is_same_v<Field, Color>)
|
||||
std::sscanf(text, "%f %f %f", &field.r, &field.g, &field.b);
|
||||
else if constexpr (std::is_floating_point_v<Field>)
|
||||
field = (Field)SDL_atof(text);
|
||||
else
|
||||
field = (Field)SDL_atoi(text);
|
||||
}
|
||||
|
||||
template <class Field> std::string field_write(const Field& field)
|
||||
{
|
||||
if constexpr (std::is_same_v<Field, std::string>)
|
||||
return field;
|
||||
else if constexpr (std::is_same_v<Field, Vec2>)
|
||||
return field_write(field.x) + " " + field_write(field.y);
|
||||
else if constexpr (std::is_same_v<Field, Color>)
|
||||
return field_write(field.r) + " " + field_write(field.g) + " " + field_write(field.b);
|
||||
else if constexpr (std::is_floating_point_v<Field>)
|
||||
return std::to_string(field);
|
||||
else
|
||||
return std::to_string((int)field);
|
||||
}
|
||||
|
||||
// Sets whichever of a struct's fields a line names.
|
||||
template <class Fields> void field_line_read(Fields& fields, const std::string& key, const char* value)
|
||||
{
|
||||
fields.visit([&](const char* name, auto& field) {
|
||||
if (key == name)
|
||||
field_read(value, field);
|
||||
});
|
||||
}
|
||||
|
||||
template <class Fields> bool is_fields_read(const std::string& path, Fields& fields)
|
||||
{
|
||||
return is_file_read(path, [&](const std::string& key, const char* value) { field_line_read(fields, key, value); });
|
||||
}
|
||||
|
||||
template <class Fields> std::string fields_write(Fields& fields)
|
||||
{
|
||||
std::string text;
|
||||
fields.visit([&](const char* name, auto& field) { text += std::string(name) + "=" + field_write(field) + "\n"; });
|
||||
return text;
|
||||
}
|
||||
|
||||
// The game's preferences, kept in settings.txt.
|
||||
struct Settings
|
||||
{
|
||||
@@ -29,23 +104,26 @@ struct Settings
|
||||
void load();
|
||||
void save();
|
||||
void reset();
|
||||
template <class Visitor> void visit(Visitor&& visitor);
|
||||
|
||||
template <class Visitor> void visit(Visitor&& visitor)
|
||||
{
|
||||
visitor("character_color", isCharacterColor);
|
||||
visitor("color", color);
|
||||
visitor("console", isConsole);
|
||||
visitor("height", height);
|
||||
visitor("last_character", lastCharacter);
|
||||
visitor("measurement", measurement);
|
||||
visitor("mute", isMuted);
|
||||
visitor("volume", volume);
|
||||
visitor("width", width);
|
||||
visitor("x", x);
|
||||
visitor("y", y);
|
||||
}
|
||||
};
|
||||
|
||||
struct SavedItem
|
||||
// A character's body as it is now: what's in it, how it digests, what it's eaten. Rates are percent per tick.
|
||||
struct Stats
|
||||
{
|
||||
std::string name;
|
||||
Vec2 position;
|
||||
Vec2 velocity;
|
||||
float rotation = 0;
|
||||
};
|
||||
|
||||
// A character's progress, kept in saves/<character>.txt, and the live state of one being played.
|
||||
struct Progress
|
||||
{
|
||||
std::vector<std::string> discovered;
|
||||
std::vector<SavedItem> items;
|
||||
Vec2 pan;
|
||||
float calories = 0;
|
||||
float capacity = 0;
|
||||
float digestionProgress = 0;
|
||||
@@ -53,17 +131,25 @@ struct Progress
|
||||
float playtime = 0;
|
||||
float totalCalories = 0;
|
||||
float weight = 0;
|
||||
float zoom = 0;
|
||||
int digestionTimer = 0;
|
||||
int spawnDelay = 0;
|
||||
int totalConsumed = 0;
|
||||
int totalSpawned = 0;
|
||||
bool isAlternate = false;
|
||||
bool isDigesting = false;
|
||||
bool isPostgame = false;
|
||||
bool isValid = false;
|
||||
|
||||
void load(const std::string& path);
|
||||
void save(const std::string& path);
|
||||
template <class Visitor> void visit(Visitor&& visitor);
|
||||
template <class Visitor> void visit(Visitor&& visitor)
|
||||
{
|
||||
visitor("alternate", isAlternate);
|
||||
visitor("calories", calories);
|
||||
visitor("capacity", capacity);
|
||||
visitor("digesting", isDigesting);
|
||||
visitor("digestion_progress", digestionProgress);
|
||||
visitor("digestion_rate", digestionRate);
|
||||
visitor("digestion_timer", digestionTimer);
|
||||
visitor("playtime", playtime);
|
||||
visitor("total_calories", totalCalories);
|
||||
visitor("total_consumed", totalConsumed);
|
||||
visitor("total_spawned", totalSpawned);
|
||||
visitor("weight", weight);
|
||||
}
|
||||
};
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
// Every piece of UI text a character can replace (in its character.json "strings"), with its default.
|
||||
#define STRINGS(X) \
|
||||
X(MenuTabInteract, "Interact") \
|
||||
X(MenuTabInventory, "Catalog") \
|
||||
X(MenuTabSettings, "Settings") \
|
||||
X(MenuTabCheats, "Cheats") \
|
||||
X(MenuOpenTooltip, "Open Main Menu") \
|
||||
X(MenuCloseTooltip, "Close Main Menu") \
|
||||
X(InteractChatButton, "Let's chat!") \
|
||||
X(InteractHelpButton, "Help") \
|
||||
X(InteractFeelingButton, "How are you feeling?") \
|
||||
X(InteractSpawnItemButton, "Spawn Item") \
|
||||
X(InteractSpawnItemCooldownFormat, "Next item in %0.2fs") \
|
||||
X(InteractStats, "Stats") \
|
||||
X(InteractWeightFormat, "Weight: %0.2f %s (Stage: %i)") \
|
||||
X(InteractCapacityFormat, "Capacity: %0.0f / %0.0f kcal") \
|
||||
X(InteractDigestionRateFormat, "Digestion Rate: %0.2f%%/sec") \
|
||||
X(InteractTotalCaloriesFormat, "Total Calories Consumed: %0.0f kcal") \
|
||||
X(InteractTotalItemsSpawnedFormat, "Total Items Spawned: %i") \
|
||||
X(InteractTotalItemsConsumedFormat, "Total Items Consumed: %i") \
|
||||
X(InteractTotalPlaytimeFormat, "Total Playtime: %s") \
|
||||
X(SettingsWindow, "Window") \
|
||||
X(SettingsWindowMode, "Mode") \
|
||||
X(SettingsWindowModeTooltip, "Switch between windowed and fullscreen display.\n(Shortcut: F)") \
|
||||
X(SettingsWindowed, "Windowed") \
|
||||
X(SettingsFullscreen, "Fullscreen") \
|
||||
X(SettingsMeasurementSystem, "Measurement System") \
|
||||
X(SettingsMetric, "Metric") \
|
||||
X(SettingsMetricTooltip, "Use kilograms/grams (kg/g).") \
|
||||
X(SettingsImperial, "Imperial") \
|
||||
X(SettingsImperialTooltip, "Use pounds/ounces (lbs/oz).") \
|
||||
X(SettingsSound, "Sound") \
|
||||
X(SettingsVolume, "Volume") \
|
||||
X(SettingsVolumeTooltip, "Adjust master volume.") \
|
||||
X(SettingsMute, "Mute") \
|
||||
X(SettingsMuteTooltip, "Mute all audio.\n(Shortcut: M)") \
|
||||
X(SettingsAppearance, "Appearance") \
|
||||
X(SettingsUseCharacterColor, "Use Character Color") \
|
||||
X(SettingsUseCharacterColorTooltip, "When playing, the UI will use the character's preset UI color.") \
|
||||
X(SettingsColor, "Color") \
|
||||
X(SettingsColorTooltip, "Change the UI color.") \
|
||||
X(SettingsResetButton, "Reset to Default") \
|
||||
X(SettingsSaveButton, "Save") \
|
||||
X(SettingsSaveTooltip, "Save the game.\n(Note: the game autosaves frequently.)") \
|
||||
X(SettingsReturnToCharactersButton, "Return to Characters") \
|
||||
X(SettingsReturnToCharactersTooltip, "Go back to the character selection screen.\nProgress will be saved.") \
|
||||
X(ToastCheatsUnlocked, "Cheats unlocked!") \
|
||||
X(ToastSaving, "Saving...") \
|
||||
X(ToastNewItemFound, "New item found!") \
|
||||
X(ToolsHomeButton, "Home") \
|
||||
X(ToolsHomeTooltip, "Reset camera view.\n(Shortcut: Home)") \
|
||||
X(ToolsOpenTooltip, "Open Tools") \
|
||||
X(ToolsCloseTooltip, "Close Tools") \
|
||||
X(ToolsAutofeedDisabledTooltip, "Disabled while Autofeed is active!") \
|
||||
X(InventoryEmptyHint, "No items.") \
|
||||
X(InventoryFlavorFormat, "Flavor: %s") \
|
||||
X(InventoryCaloriesFormat, "%0.0f kcal") \
|
||||
X(InventoryCapacityBonusFormat, "Capacity Bonus: +%0.0f kcal") \
|
||||
X(InventoryDigestionRateBonusFormat, "Digestion Rate Bonus: +%0.2f%% / sec") \
|
||||
X(InventoryDigestionRatePenaltyFormat, "Digestion Rate Penalty: %0.2f%% / sec") \
|
||||
X(InventoryEnableAutofeedButton, "Enable Autofeed") \
|
||||
X(InventoryDisableAutofeedButton, "Disable Autofeed") \
|
||||
X(InventoryEnableAutofeedTooltip, "Serve the character food and assist in digestion automatically.") \
|
||||
X(InventoryDisableAutofeedTooltip, "Return to manual interaction.") \
|
||||
X(InfoProgressMax, "MAX") \
|
||||
X(InfoProgressToNextStage, "To Next Stage") \
|
||||
X(InfoStageProgressFormat, "Stage: %i/%i (%0.1f%%)") \
|
||||
X(InfoMaxedOut, "Maxed out!") \
|
||||
X(InfoStageStartFormat, "Start: %0.2f %s") \
|
||||
X(InfoStageCurrentFormat, "Current: %0.2f %s") \
|
||||
X(InfoStageNextFormat, "Next: %0.2f %s") \
|
||||
X(InfoDigestion, "Digestion") \
|
||||
X(InfoDigesting, "Digesting...") \
|
||||
X(InfoDigestionInProgress, "Digestion in progress...") \
|
||||
X(InfoGiveFoodToStartDigesting, "Give food to start digesting!") \
|
||||
X(InfoDigestionRateFormat, "Rate: %0.2f%% / sec") \
|
||||
X(CheatsCalories, "Calories") \
|
||||
X(CheatsCapacity, "Capacity") \
|
||||
X(CheatsWeight, "Weight") \
|
||||
X(CheatsWeightFormat, "%0.2f kg") \
|
||||
X(CheatsStage, "Stage") \
|
||||
X(CheatsDigestionRate, "Digestion Rate") \
|
||||
X(CheatsDigestionRateFormat, "%0.2f%% / sec") \
|
||||
X(CheatsDigestButton, "Digest")
|
||||
|
||||
enum StringId
|
||||
{
|
||||
#define X(name, text) STRING_##name,
|
||||
STRINGS(X)
|
||||
#undef X
|
||||
STRING_COUNT
|
||||
};
|
||||
|
||||
constexpr const char* STRING_NAMES[] = {
|
||||
#define X(name, text) #name,
|
||||
STRINGS(X)
|
||||
#undef X
|
||||
};
|
||||
|
||||
constexpr const char* STRING_DEFAULTS[] = {
|
||||
#define X(name, text) text,
|
||||
STRINGS(X)
|
||||
#undef X
|
||||
};
|
||||
+624
-415
File diff suppressed because it is too large.
Load diff
@@ -18,52 +18,63 @@ enum Direction
|
||||
DIRECTION_RIGHT
|
||||
};
|
||||
|
||||
// Hover and select sounds (from the character being played, or none) and the pulsing border on hovered widgets.
|
||||
void widget_sounds_set(Audio* audio, const Sounds* hover, const Sounds* select);
|
||||
void widget_fx();
|
||||
|
||||
// Passes a widget's result through after giving it its sounds and hover border.
|
||||
inline bool is_fx(bool result)
|
||||
enum CommandType
|
||||
{
|
||||
widget_fx();
|
||||
return result;
|
||||
}
|
||||
COMMAND_CONTINUE,
|
||||
COMMAND_NEW_GAME,
|
||||
COMMAND_MENU,
|
||||
COMMAND_SAVE,
|
||||
COMMAND_RELOAD,
|
||||
COMMAND_FULLSCREEN
|
||||
};
|
||||
|
||||
inline ImVec4 imvec4_get(Color color)
|
||||
// A request, from the UI or the console, for the game to change what's running. Character names a character's folder.
|
||||
struct Command
|
||||
{
|
||||
return {color.r, color.g, color.b, color.a};
|
||||
}
|
||||
std::string character;
|
||||
CommandType type = COMMAND_MENU;
|
||||
};
|
||||
|
||||
using Commands = std::vector<Command>;
|
||||
|
||||
void style_set(Color color, float rounding = 0);
|
||||
float row_width_get(int count);
|
||||
void tooltip_set(const char* text, float size = FONT_NORMAL);
|
||||
void triangle_draw(ImVec2 position, ImVec2 size, Direction direction);
|
||||
|
||||
// The menus, tools, toasts and select screen.
|
||||
// The menus, tools, dialogue box, toasts and select screen.
|
||||
class Ui
|
||||
{
|
||||
public:
|
||||
std::vector<Actor> itemActors;
|
||||
std::vector<Canvas> itemCanvases;
|
||||
std::vector<ImVec2> itemSizes;
|
||||
Actor previewActor;
|
||||
Canvas previewCanvas;
|
||||
Progress selectedProgress;
|
||||
Stats selectedStats;
|
||||
Audio* audio = nullptr;
|
||||
Character* catalogCharacter = nullptr;
|
||||
Character* character = nullptr;
|
||||
Commands* commands = nullptr;
|
||||
Renderer* renderer = nullptr;
|
||||
float menuSlide = 1;
|
||||
float toolsSlide = 0;
|
||||
int characterIndex = 0;
|
||||
int progressIndex = -1;
|
||||
int selectedIndex = -1;
|
||||
int selectedItem = -1;
|
||||
bool isContinue = false;
|
||||
bool isFullscreen = false;
|
||||
bool isFullscreenToggled = false;
|
||||
bool isNewGame = false;
|
||||
bool isNewGameWarning = false;
|
||||
bool isPreview = false;
|
||||
bool isRefresh = false;
|
||||
bool isReturn = false;
|
||||
bool isSave = false;
|
||||
bool isSelectedSaved = false;
|
||||
|
||||
void play_update(Play& play, Settings& settings, Renderer& renderer, Audio& audio);
|
||||
void select_update(std::vector<Character>& characters, Settings& settings, Renderer& renderer, Audio& audio);
|
||||
void settings_update(Settings& settings, Audio& audio, const Character* character);
|
||||
void play_update(Play& play, Settings& settings);
|
||||
void select_update(std::vector<Character>& characters, Settings& settings);
|
||||
|
||||
private:
|
||||
bool is_fx(bool result);
|
||||
void command_add(CommandType type, const std::string& key = "");
|
||||
void settings_draw(Settings& settings);
|
||||
void interact_tab_draw(Play& play, Settings& settings);
|
||||
void catalog_tab_draw(Play& play);
|
||||
void cheats_tab_draw(Play& play);
|
||||
void tools_draw(Play& play, ImVec2 position, ImVec2 size);
|
||||
void bar_draw(Play& play, bool& isOpen, float slide, ImVec2 position, ImVec2 size, Direction direction,
|
||||
const char* openTooltip, const char* closeTooltip);
|
||||
void text_draw(Play& play, ImVec2 screen);
|
||||
void toasts_draw(Play& play);
|
||||
};
|
||||
Reference in new issue
Block a user