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
+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;
|
||||
}
|
||||
Reference in new issue
Block a user