Juice: pitch variation, squash, dust, trails, jiggle, breathing, hit-stop, numbers, stage-up, digest and rare flair
This commit is contained in:
1 parent
7e27ffad94
commit
fdfea89373
11 files changed
+361
-19
No files matched your search
+3
-1
@@ -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
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+36
-3
@@ -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,6 +202,9 @@ void Body::update(std::vector<Event>& events)
|
||||
if (!isTalking)
|
||||
idle_play();
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+72
-8
@@ -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))
|
||||
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
@@ -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
@@ -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
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
Reference in new issue
Block a user