Juice: pitch variation, squash, dust, trails, jiggle, breathing, hit-stop, numbers, stage-up, digest and rare flair

This commit is contained in:
shweet committed 2026-11-13 00:45:55 -05:00
1 parent 7e27ffad94
commit fdfea89373
11 files changed
+363 -21

No files matched your search

+3 -1
View File
@@ -150,6 +150,8 @@ The areas which will expand based on how full the character is and their progres
| `null` | The null that will expand. |
| `scaleMax` | The scale (in percent) that will be added at maximum. Know that a scale of 100 is the normal scale of an animation. |
The first expand area is treated as the belly: it breathes, jiggles when smacked, rubbed or fed, and is where digestion and stage-up effects come from.
### interactTypes, interactAreas
`interactTypes` is an object of interaction types by name, giving each type's defaults. Rub, Smack, Kiss and Turn Around come from common. A type gets a button on the taskbar only if one of the character's areas uses it. `interactAreas` is a list of the concrete areas on a character which can be interacted with; usually for belly rubs, kisses, etc. Each area names its `type` and takes all of that type's settings, changing whichever it gives itself.
@@ -199,7 +201,7 @@ The UI's sounds.
| `sounds` | `open` and `close` (the taskbar or one of its windows), `hover` and `select` (a widget), and `cheats` (after entering a special code to activate cheats). |
### particles
An object of particles by name, each with the `anm2` and `animation` it plays once where spawned.
An object of particles by name, each with the `anm2` and `animation` it plays once where spawned. The `Puff` particle is also used for the dust when an item lands hard.
# items.json
+3
View File
@@ -2,6 +2,8 @@
#include <SDL3/SDL.h>
#include <stb_vorbis.c>
constexpr float PITCH_VARIATION = 0.06f;
void Audio::init()
{
if (SDL_InitSubSystem(SDL_INIT_AUDIO))
@@ -88,6 +90,7 @@ void Audio::play(Sound* sound)
SDL_AudioSpec spec = {SDL_AUDIO_S16, sound->channels, sound->rate};
SDL_ClearAudioStream(voices[voice]);
SDL_SetAudioStreamFormat(voices[voice], &spec, nullptr);
SDL_SetAudioStreamFrequencyRatio(voices[voice], random_get(1 - PITCH_VARIATION, 1 + PITCH_VARIATION));
SDL_PutAudioStreamData(voices[voice], sound->samples, sound->frames * sound->channels * (int)sizeof(short));
}
+37 -4
View File
@@ -1,5 +1,9 @@
#include "play.h"
constexpr float BREATH_AMPLITUDE = 1.5f;
constexpr float BREATH_SPEED = 2.2f;
constexpr float JIGGLE_DAMPING = 0.12f;
constexpr float JIGGLE_STIFFNESS = 0.18f;
constexpr float SWAP_STEP = 0.5f;
enum SwapMode
@@ -58,6 +62,20 @@ float Body::capacity_percent_get()
return capacityMax > 0 ? stats.calories / capacityMax : 0;
}
// Pushes the belly's spring, harder the bigger the body has grown.
void Body::jiggle_add(float amount)
{
jiggleVelocity += amount * (1 + (float)stage / std::max(1, (int)character->stages.size()));
}
// Where the belly is (its expand area's null), or else the middle of the body.
Vec2 Body::belly_get()
{
std::vector<ExpandArea>& areas = character->expandAreas;
Rect rect = areas.empty() ? Rect{} : actor.null_rect_get(areas[0].null);
return rect_middle(is_rect_valid(rect) ? rect : actor.bounds_get());
}
bool Body::is_over_capacity()
{
return stats.calories > stats.capacity;
@@ -184,7 +202,10 @@ void Body::update(std::vector<Event>& events)
if (!isTalking)
idle_play();
}
actor.update(*audio);
if (freeze > 0)
freeze--;
else
actor.update(*audio);
if (stats.isDigesting && --stats.digestionTimer <= 0)
{
@@ -205,9 +226,12 @@ void Body::update(std::vector<Event>& events)
stage_set(nextStage);
isStageUp = true;
actor.isLocked = true;
events.push_back(EVENT_STAGE_START);
}
else
events.push_back(EVENT_DIGESTED);
gained = nextWeight - stats.weight;
events.push_back(EVENT_GAINED);
stats.weight = nextWeight;
stats.isDigesting = false;
stats.digestionTimer = traits.digestionTime;
@@ -222,6 +246,7 @@ void Body::update(std::vector<Event>& events)
stats.isDigesting = true;
stats.digestionTimer = traits.digestionTime;
audio->play(character->sounds[SOUND_DIGEST]);
events.push_back(EVENT_DIGEST_START);
}
}
values_clamp();
@@ -234,13 +259,21 @@ void Body::update(std::vector<Event>& events)
float capacityProgress = capacity_percent_get();
if (stats.isDigesting)
capacityProgress *= (float)stats.digestionTimer / std::max(1, traits.digestionTime);
for (ExpandArea& expandArea : character->expandAreas)
jiggleVelocity += -jiggle * JIGGLE_STIFFNESS - jiggleVelocity * JIGGLE_DAMPING;
jiggle += jiggleVelocity;
float growth = 1 + (float)stage / std::max(1, (int)character->stages.size());
float breath = std::sin(stats.playtime * BREATH_SPEED) * BREATH_AMPLITUDE * growth;
for (int i = 0; i < (int)character->expandAreas.size(); i++)
{
ExpandArea& expandArea = character->expandAreas[i];
float add = std::clamp((stageProgress + capacityProgress) * expandArea.scaleMax / 2, 0.0f, expandArea.scaleMax);
Vec2 scale = {add, add};
if (i == 0)
scale = scale + Vec2{jiggle, breath - jiggle};
if (expandArea.layer >= 0)
actor.layers[expandArea.layer].scale = {add, add};
actor.layers[expandArea.layer].scale = scale;
if (expandArea.null >= 0)
actor.nullScales[expandArea.null] = {add, add};
actor.nullScales[expandArea.null] = scale;
}
isTalking = is_swapping(actor, character->talk, talkTime, isTalking, SWAP_LOOP);
+50
View File
@@ -0,0 +1,50 @@
#include "effects.h"
constexpr float FLASH_FADE = 0.04f;
constexpr float FLOATER_RISE = 0.8f;
constexpr float SPARK_DRAG = 0.95f;
constexpr float SPEED_MIN = 0.4f;
void Effects::update()
{
for (int i = 0; i < (int)floaters.size(); i++)
{
floaters[i].position.y -= FLOATER_RISE;
if (--floaters[i].life <= 0)
floaters.erase(floaters.begin() + i--);
}
for (int i = 0; i < (int)sparks.size(); i++)
{
Spark& spark = sparks[i];
spark.velocity = spark.velocity * SPARK_DRAG;
spark.velocity.y += spark.gravity;
spark.position = spark.position + spark.velocity;
if (--spark.life <= 0)
sparks.erase(sparks.begin() + i--);
}
flash = std::max(0.0f, flash - FLASH_FADE);
}
void Effects::floater_add(Vec2 position, float value, FloaterType type)
{
floaters.push_back({position, value, FLOATER_LIFE, type});
}
void Effects::spark_add(Spark spark)
{
spark.lifeMax = spark.life;
sparks.push_back(spark);
}
// Throws count copies of a spark out in every direction, at up to speed.
void Effects::burst_add(Spark spark, int count, float speed)
{
for (int i = 0; i < count; i++)
{
float angle = random_get(0, 2 * PI);
float magnitude = random_get(speed * SPEED_MIN, speed);
Spark each = spark;
each.velocity = Vec2{std::cos(angle), std::sin(angle)} * magnitude + spark.velocity;
spark_add(each);
}
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include "common.h"
#include <vector>
constexpr int FLOATER_LIFE = 75;
enum FloaterType
{
FLOATER_CALORIES,
FLOATER_WEIGHT
};
enum SparkShape
{
SPARK_CIRCLE,
SPARK_STAR
};
// A number rising from where something happened, fading as it goes.
struct Floater
{
Vec2 position;
float value = 0;
int life = 0;
FloaterType type = FLOATER_CALORIES;
};
// A glint or puff that drifts, falls (or rises) by its gravity, and fades over its life.
struct Spark
{
Color color;
Vec2 position;
Vec2 velocity;
float gravity = 0;
float size = 0;
int life = 0;
int lifeMax = 0;
SparkShape shape = SPARK_CIRCLE;
};
// Short flourishes over the world: rising numbers, sparks, and the screen flashing.
class Effects
{
public:
std::vector<Floater> floaters;
std::vector<Spark> sparks;
float flash = 0;
void update();
void floater_add(Vec2 position, float value, FloaterType type);
void spark_add(Spark spark);
void burst_add(Spark spark, int count, float speed);
};
+12 -2
View File
@@ -1,6 +1,9 @@
#include "play.h"
constexpr const char* FULL_SUFFIX = "Full";
constexpr float RUB_JIGGLE = 0.3f;
constexpr float SMACK_JIGGLE = 6;
constexpr int HIT_STOP = 3;
constexpr float AUTO_ACCELERATION = 0.45f;
constexpr float AUTO_ANGLE_STEP = 0.08f;
@@ -63,6 +66,11 @@ void Interactions::update(Play& play, Pointer pointer)
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);
if (entry.scaleAmplitude > 0)
{
body.jiggle_add(SMACK_JIGGLE);
body.freeze = HIT_STOP;
}
bool isReady = cooldowns[i] <= 0;
cooldowns[i] = std::max(0, entry.dialogueCooldown);
Dialogue& dialogue = character.dialogue;
@@ -82,8 +90,10 @@ void Interactions::update(Play& play, Pointer pointer)
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))
bool isMoved = moved.x != 0 || moved.y != 0;
if (entry.digestionHover > 0 && isMoved)
body.jiggle_add(RUB_JIGGLE);
if (entry.digestionHover > 0 && body.stats.calories > 0 && !body.stats.isDigesting && isMoved)
body.stats.digestionProgress += entry.digestionHover;
}
}
+73 -9
View File
@@ -8,7 +8,26 @@ constexpr float CRUMB_SPEED_X = 4;
constexpr float CRUMB_SPEED_Y_MAX = 7;
constexpr float CRUMB_SPEED_Y_MIN = 2;
constexpr float CRUMB_SPIN = 15;
constexpr Color GOLD = {1, 0.85f, 0.3f, 1};
constexpr Color TRAIL_COLOR = {1, 1, 1, 0.35f};
constexpr const char* DUST_PARTICLE = "Puff";
constexpr float BITE_JIGGLE = 4;
constexpr float DUST_IMPACT = 12;
constexpr float FANFARE_MIN = 0.75f;
constexpr float FANFARE_SHAKE = 0.015f;
constexpr float FANFARE_SPEED = 5;
constexpr float HOLD_SPIN = 0.1f;
constexpr float RARE_SPARKLE_CHANCE = 12;
constexpr float RARE_SPARKLE_RISE = -0.5f;
constexpr float SQUASH_DECAY = 0.82f;
constexpr float SQUASH_MAX = 0.35f;
constexpr float SQUASH_PER_SPEED = 0.025f;
constexpr float TRAIL_SPEED = 12;
constexpr Spark FANFARE_SPARK = {GOLD, {}, {}, 0.05f, 16, 50, 0, SPARK_STAR};
constexpr Spark RARE_SPARK = {GOLD, {}, {}, 0, 10, 40, 0, SPARK_STAR};
constexpr Spark TRAIL_SPARK = {TRAIL_COLOR, {}, {}, 0, 7, 14, 0, SPARK_CIRCLE};
constexpr int FANFARE_COUNT = 20;
constexpr int FANFARE_SHAKE_TIME = 12;
constexpr int CRUMB_COUNT = 6;
constexpr float ROTATION_GROUND_DAMPING = 0.85f;
constexpr float ROTATION_MAX = 90;
@@ -17,7 +36,7 @@ constexpr float THROW_THRESHOLD = 10;
void Items::add(Play& play, int id, Vec2 position, Vec2 velocity, float rotation)
{
ItemEntity& entity = list.emplace_back(ItemEntity{play.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.rotation = rotation;
discovered[id] = true;
@@ -46,8 +65,17 @@ void Items::crumbs_spawn(ItemEntity& source)
}
}
// Moves an item or crumb a tick under gravity, bouncing off the bounds and settling on the ground. True if it bounced.
static bool motion_update(Play& play, ItemEntity& entity)
// How special an item's rarity is, from 0 (the first, or none) to 1 (the last).
static float rarity_intensity_get(Play& play, int id)
{
int rarity = play.character->catalog.items[id].rarity;
int count = (int)play.character->catalog.rarities.size();
return rarity > 0 && count > 1 ? (float)rarity / (count - 1) : 0;
}
// Moves an item or crumb a tick under gravity, bouncing off the bounds and settling on the ground. Gives the speed it
// hit something at (0 if it didn't), and squashes it by that.
static float motion_update(Play& play, ItemEntity& entity)
{
Area& area = play.character->area;
Rect bounds = play.bounds;
@@ -58,16 +86,17 @@ static bool motion_update(Play& play, ItemEntity& entity)
gravity = gravity != 0 ? gravity : area.gravity;
float right = bounds.x + bounds.w;
float bottom = bounds.y + bounds.h;
bool isBounced = false;
float impact = 0;
position = position + velocity;
rotation += entity.angularVelocity;
if (position.x <= bounds.x || position.x >= right)
{
position.x = position.x <= bounds.x ? bounds.x + 1 : right - 1;
impact = std::abs(velocity.x);
entity.squash = Vec2{-1, 1} * std::min(SQUASH_MAX, impact * SQUASH_PER_SPEED);
velocity.x = -velocity.x * area.friction;
entity.angularVelocity = -entity.angularVelocity * area.friction;
rotation = -rotation;
isBounced = true;
}
if (position.y <= bounds.y || position.y >= bottom)
{
@@ -82,16 +111,17 @@ static bool motion_update(Play& play, ItemEntity& entity)
}
else
{
impact = std::abs(velocity.y);
entity.squash = Vec2{1, -1} * std::min(SQUASH_MAX, impact * SQUASH_PER_SPEED);
velocity.y = -velocity.y;
entity.angularVelocity *= area.friction;
isBounced = true;
}
velocity.y *= area.friction;
}
velocity.x *= area.airResistance;
velocity.y += gravity;
position = {std::clamp(position.x, bounds.x, right), std::clamp(position.y, bounds.y, bottom)};
return isBounced;
return impact;
}
// Forgets which item is being eaten, and lets go of the held one unless asked to keep it.
@@ -209,6 +239,13 @@ void Items::update(Play& play, Pointer pointer)
audio.play(character.sounds[SOUND_NEW_ITEM]);
}
add(play, id, position);
if (rarity_intensity_get(play, id) >= FANFARE_MIN)
{
Spark spark = FANFARE_SPARK;
spark.position = position;
play.effects.burst_add(spark, FANFARE_COUNT, FANFARE_SPEED);
play.camera.shake_add(FANFARE_SHAKE, FANFARE_SHAKE_TIME);
}
stats.totalSpawned++;
play.particles_spawn(character.catalog.summonParticles, position);
}
@@ -256,6 +293,8 @@ void Items::update(Play& play, Pointer pointer)
traits.digestionRateMin, traits.digestionRateMax);
stats.totalConsumed++;
crumbs_spawn(entity);
play.effects.floater_add(entity.actor.position, item.calories, FLOATER_CALORIES);
body.jiggle_add(BITE_JIGGLE);
removed = held;
drop();
feeding.animation = body.actor.animation;
@@ -297,6 +336,7 @@ void Items::update(Play& play, Pointer pointer)
if (isThrown)
text_play(HOOK_THROW);
entity.velocity = entity.velocity - delta;
entity.isThrown = isThrown;
release(isThrown);
}
if (removed >= 0)
@@ -347,8 +387,32 @@ void Items::update(Play& play, Pointer pointer)
position = {std::clamp(position.x, bounds.x, bounds.x + bounds.w),
std::clamp(position.y, bounds.y, bounds.y + bounds.h)};
}
else if (motion_update(play, entity))
audio.play(character.sounds[SOUND_BOUNCE]);
else
{
float impact = motion_update(play, entity);
if (impact > 0)
audio.play(character.sounds[SOUND_BOUNCE]);
if (impact >= DUST_IMPACT)
play.particles_spawn({DUST_PARTICLE}, position);
entity.isThrown &= vec2_length(entity.velocity) >= TRAIL_SPEED;
if (entity.isThrown)
{
Spark spark = TRAIL_SPARK;
spark.position = position;
play.effects.spark_add(spark);
}
}
entity.squash = entity.squash * SQUASH_DECAY;
for (LayerChange& layer : entity.actor.layers)
layer.scale = entity.squash * PERCENT;
if (i != held && is_chance(rarity_intensity_get(play, entity.id) * RARE_SPARKLE_CHANCE))
{
Rect rect = entity.actor.bounds_get();
Spark spark = RARE_SPARK;
spark.position = {rect.x + random_get(0, rect.w), rect.y + random_get(0, rect.h)};
spark.velocity = {0, RARE_SPARKLE_RISE};
play.effects.spark_add(spark);
}
}
for (int i = 0; i < (int)crumbs.size(); i++)
+45 -1
View File
@@ -11,7 +11,24 @@ constexpr ImGuiKey CHEAT_KEYS[] = {ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGu
ImGuiKey_RightArrow, ImGuiKey_B, ImGuiKey_A};
constexpr const char* KEY_DISCOVERED = "discovered";
constexpr const char* KEY_ITEM = "item";
constexpr Spark DIGEST_SPARK = {{1, 1, 1, 0.5f}, {}, {}, -0.06f, 10, 50, 0, SPARK_CIRCLE};
constexpr Spark STAGE_SPARK = {{1, 0.85f, 0.3f, 1}, {}, {}, 0.06f, 18, 70, 0, SPARK_STAR};
constexpr float AUTOSAVE_TIME = 30;
constexpr float DIGEST_JIGGLE = 3;
constexpr float DIGEST_SHAKE = 0.006f;
constexpr float DIGEST_SPEED = 2;
constexpr float PUNCH_DECAY = 0.93f;
constexpr float PUNCH_EPSILON = 0.001f;
constexpr float STAGE_FLASH = 0.7f;
constexpr float STAGE_JIGGLE = 15;
constexpr float STAGE_PUNCH = 0.08f;
constexpr float STAGE_SHAKE = 0.04f;
constexpr float STAGE_SPEED = 8;
constexpr int DIGEST_COUNT = 10;
constexpr int DIGEST_SHAKE_TIME = 40;
constexpr int STAGE_COUNT = 28;
constexpr int STAGE_FREEZE = 5;
constexpr int STAGE_SHAKE_TIME = 30;
constexpr float CHEAT_TIME = 5;
constexpr float FOCUS_PADDING = 100;
constexpr float SHAKE_DECAY = 0.97f;
@@ -176,6 +193,25 @@ void Play::events_handle(Vec2 screenSize)
Dialogue& dialogue = character->dialogue;
for (Event event : events)
{
Spark spark = event == EVENT_STAGE_START ? STAGE_SPARK : DIGEST_SPARK;
spark.position = body.belly_get();
if (event == EVENT_GAINED && body.gained > 0)
effects.floater_add(spark.position, body.gained, FLOATER_WEIGHT);
if (event == EVENT_DIGEST_START)
{
effects.burst_add(spark, DIGEST_COUNT, DIGEST_SPEED);
camera.shake_add(DIGEST_SHAKE, DIGEST_SHAKE_TIME);
body.jiggle_add(DIGEST_JIGGLE);
}
if (event == EVENT_STAGE_START)
{
effects.burst_add(spark, STAGE_COUNT, STAGE_SPEED);
effects.flash = STAGE_FLASH;
camera.shake_add(STAGE_SHAKE, STAGE_SHAKE_TIME);
camera.punch = STAGE_PUNCH;
body.jiggle_add(STAGE_JIGGLE);
body.freeze = STAGE_FREEZE;
}
if (event == EVENT_DIGESTED && !text.isScene && text.is_interruptible())
text.set(*this, dialogue.hook_get(HOOK_DIGEST));
if (event == EVENT_STAGE_UP && !text.isScene)
@@ -240,6 +276,7 @@ void Play::update(Vec2 screenSize)
area_set();
area.update(*audio);
effects.update();
for (int i = 0; i < (int)particles.size(); i++)
{
particles[i].update(*audio);
@@ -290,10 +327,14 @@ Vec2 Camera::world_get(Vec2 screen)
return pan + screen / (zoom / PERCENT);
}
// The world's transform to the screen, zoomed further about the screen's middle by the punch.
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};
Vec2 offset = Vec2{-pan.x * scale + shake.x * screenSize.x, -pan.y * scale + shake.y * screenSize.y} * (1 + punch) -
screenSize / 2 * punch;
scale *= 1 + punch;
return {scale, 0, 0, scale, offset.x, offset.y};
}
void Camera::shake_add(float magnitude, int time)
@@ -344,6 +385,9 @@ void Camera::update(Play& play, Vec2 screenSize)
if (ImGui::IsKeyPressed(ImGuiKey_Home))
play.focus(screenSize);
punch = punch * PUNCH_DECAY;
if (punch < PUNCH_EPSILON)
punch = 0;
if (shakeTime > 0)
{
shakeTime--;
+18 -2
View File
@@ -2,6 +2,7 @@
#include "character.h"
#include "dialogue.h"
#include "effects.h"
#include "save.h"
constexpr float DIGESTION_MAX = 100;
@@ -31,7 +32,10 @@ enum Hold
// What happened to the body in a tick, answered once it's done.
enum Event
{
EVENT_DIGEST_START,
EVENT_DIGESTED,
EVENT_GAINED,
EVENT_STAGE_START,
EVENT_STAGE_UP,
EVENT_STAGE_FINAL
};
@@ -50,14 +54,17 @@ struct Pointer
bool isRightClicked = false;
};
// An item in the world, or a crumb of one (which has a life, in ticks).
// An item in the world, or a crumb of one (which has a life, in ticks). Squash is the added scale of its last impact,
// and a thrown item trails until it slows down.
struct ItemEntity
{
Actor actor;
Vec2 squash;
Vec2 velocity;
float angularVelocity = 0;
int id = 0;
int life = 0;
bool isThrown = false;
};
struct Toast
@@ -68,7 +75,8 @@ struct Toast
class Play;
// The character's body: its stats, digestion and stages, and the animations they choose.
// The character's body: its stats, digestion and stages, and the animations they choose. The belly (the first expand
// area) breathes and jiggles on a spring, and freeze holds the body still for a hit's few ticks.
class Body
{
public:
@@ -79,7 +87,11 @@ public:
Character* character = nullptr;
Audio* audio = nullptr;
float blinkTime = 0;
float gained = 0;
float jiggle = 0;
float jiggleVelocity = 0;
float talkTime = 0;
int freeze = 0;
int stage = 0;
bool isBlinking = false;
bool isStageUp = false;
@@ -93,7 +105,9 @@ public:
void idle_override_set(const std::string& animation);
void stage_set(int index);
void values_clamp();
void jiggle_add(float amount);
std::string idle_get();
Vec2 belly_get();
Rect eat_rect_get(const EatArea& eatArea);
float capacity_max_get();
float capacity_percent_get();
@@ -180,6 +194,7 @@ class Camera
public:
Vec2 pan;
Vec2 shake;
float punch = 0;
float zoom = ZOOM_LADDER[0];
int shakeTime = 0;
int shakeTimeMax = 0;
@@ -202,6 +217,7 @@ public:
Autofeed autofeed;
Body body;
Camera camera;
Effects effects;
Interactions interactions;
Items items;
Text text;
+66 -2
View File
@@ -25,6 +25,14 @@ constexpr float INDICATOR_SIZE = 0.35f;
constexpr float INDICATOR_SPEED = 2;
constexpr float ITEM_SIZE = 96;
constexpr float ROUNDING = 10;
constexpr ImVec4 CALORIES_COLOR = {1, 0.85f, 0.3f, 1};
constexpr float BAR_EASE = 6;
constexpr float BAR_SHAKE = 2;
constexpr float BAR_SHAKE_SPEED = 40;
constexpr float FLOATER_OUTLINE = 2;
constexpr float OVERSTUFFED_PULSE = 0.35f;
constexpr float OVERSTUFFED_SPEED = 6;
constexpr float STAR_WAIST = 0.3f;
constexpr float BAR_WIDTH_MIN = 120;
constexpr float BUTTON_SIZE_MAX = 96;
constexpr float BUTTON_SIZE_MIN = 56;
@@ -558,9 +566,16 @@ void Ui::taskbar_draw(Play& play, ImVec2 screen)
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(labels[i]);
ImGui::SameLine();
float time = (float)ImGui::GetTime();
barFills[i] += (fills[i] - barFills[i]) * std::min(1.0f, BAR_EASE * ImGui::GetIO().DeltaTime);
if (i == 1 && overstuffed > 0)
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(1, 1 - overstuffed, 1 - overstuffed, 1));
ImGui::ProgressBar(fills[i], {widths[i], 0}, bars[i].c_str());
{
float red = 1 - overstuffed * (1 - OVERSTUFFED_PULSE * (1 + std::sin(time * OVERSTUFFED_SPEED)) / 2);
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(1, red, red, 1));
ImVec2 cursor = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos({cursor.x + std::sin(time * BAR_SHAKE_SPEED) * BAR_SHAKE * overstuffed, cursor.y});
}
ImGui::ProgressBar(barFills[i], {widths[i], 0}, bars[i].c_str());
if (i == 1 && overstuffed > 0)
ImGui::PopStyleColor();
if (i == 0 && ImGui::BeginItemTooltip())
@@ -709,6 +724,54 @@ void Ui::text_draw(Play& play, ImVec2 screen)
ImGui::PopFont();
}
// The flourishes over the world: sparks, rising numbers (outlined so they read on anything), and the screen's flash.
void Ui::effects_draw(Play& play, ImVec2 screen)
{
Effects& effects = play.effects;
Affine view = play.camera.view_get({screen.x, screen.y});
ImDrawList* drawList = ImGui::GetBackgroundDrawList();
for (Spark& spark : effects.sparks)
{
Vec2 at = view * spark.position;
float fade = (float)spark.life / std::max(1, spark.lifeMax);
float size = spark.size * view.a * fade;
ImU32 color = ImGui::GetColorU32(ImVec4(spark.color.r, spark.color.g, spark.color.b, spark.color.a * fade));
if (spark.shape == SPARK_CIRCLE)
{
drawList->AddCircleFilled({at.x, at.y}, size, color);
continue;
}
float waist = size * STAR_WAIST;
drawList->AddQuadFilled({at.x, at.y - size}, {at.x + waist, at.y}, {at.x, at.y + size}, {at.x - waist, at.y},
color);
drawList->AddQuadFilled({at.x - size, at.y}, {at.x, at.y - waist}, {at.x + size, at.y}, {at.x, at.y + waist},
color);
}
ImFont* font = ImGui::GetFont();
for (Floater& floater : effects.floaters)
{
std::string text = floater.type == FLOATER_CALORIES ? text_format("+%.0f kcal", floater.value)
: "+" + weight_format(floater.value, *measurement);
float alpha = std::min(1.0f, (float)floater.life / (FLOATER_LIFE / 2));
ImVec4 color = floater.type == FLOATER_CALORIES ? CALORIES_COLOR : ImGui::GetStyleColorVec4(ImGuiCol_PlotHistogram);
color.w = alpha;
ImVec2 size = font->CalcTextSizeA(FONT_HEADER_2, FLT_MAX, 0, text.c_str());
Vec2 at = view * floater.position;
ImVec2 position = {at.x - size.x / 2, at.y - size.y / 2};
ImU32 outline = ImGui::GetColorU32(ImVec4(0, 0, 0, alpha));
for (int x = -1; x <= 1; x++)
for (int y = -1; y <= 1; y++)
if (x != 0 || y != 0)
drawList->AddText(font, FONT_HEADER_2,
{position.x + x * FLOATER_OUTLINE, position.y + y * FLOATER_OUTLINE}, outline,
text.c_str());
drawList->AddText(font, FONT_HEADER_2, position, ImGui::GetColorU32(color), text.c_str());
}
if (effects.flash > 0)
drawList->AddRectFilled({0, 0}, screen, ImGui::GetColorU32(ImVec4(1, 1, 1, effects.flash)));
}
// Fading messages stacked down from the top right, newest first.
void Ui::toasts_draw(Play& play)
{
@@ -994,6 +1057,7 @@ void Ui::play_update(Play& play, Settings& settings)
character = play.character;
measurement = &settings.measurement;
ImVec2 screen = ImGui::GetIO().DisplaySize;
effects_draw(play, screen);
if (play.items.tooltip >= 0 && !play.isBlocked && ImGui::BeginTooltip())
{
item_details_draw(*character, &character->catalog.items[play.items.tooltip]);
+2
View File
@@ -70,6 +70,7 @@ public:
Commands* commands = nullptr;
Measurement* measurement = nullptr;
Renderer* renderer = nullptr;
float barFills[3] = {};
float buttonSize = 0;
float taskbarSlide = 1;
int characterIndex = 0;
@@ -100,4 +101,5 @@ private:
void settings_draw(Settings& settings);
void text_draw(Play& play, ImVec2 screen);
void toasts_draw(Play& play);
void effects_draw(Play& play, ImVec2 screen);
};