595 lines
24 KiB
C++
595 lines
24 KiB
C++
#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)
|
|
{
|
|
ItemEntity& entity = items.emplace_back(ItemEntity{character->item_actor_get(id), velocity, 0, id});
|
|
entity.actor.position = position;
|
|
entity.actor.overrides.push_back({.rotation = rotation, .target = TARGET_ROOT, .isRotation = true});
|
|
discovered[id] = true;
|
|
}
|
|
|
|
int Play::spawn_delay_get()
|
|
{
|
|
return std::max(1, (int)std::round(character->spawnDelay * std::pow(character->spawnDelayMultiplier, 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)
|
|
return 0;
|
|
return std::max(0.0f, play.character->rarities[item.rarity].weight);
|
|
}
|
|
|
|
bool Play::is_spawn_possible()
|
|
{
|
|
if ((int)items.size() >= character->spawnLimit)
|
|
return false;
|
|
for (int i = 0; i < (int)character->items.size(); i++)
|
|
if (spawn_weight_get(*this, i) > 0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
void Play::spawn_queue_add(int id)
|
|
{
|
|
if (id < 0 || id >= (int)character->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);
|
|
spawnQueue.push_back(id);
|
|
spawnDelay = spawn_delay_get();
|
|
}
|
|
|
|
// Queues an item picked at random by rarity, once the spawn delay is up.
|
|
void Play::spawn_queue_add()
|
|
{
|
|
if (spawnDelay > 0 || !is_spawn_possible())
|
|
return;
|
|
float total = 0;
|
|
for (int i = 0; i < (int)character->items.size(); i++)
|
|
total += spawn_weight_get(*this, 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);
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
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) {
|
|
if (!pointer.isAutomated)
|
|
text.set(*this, data.dialogue.hook_get(hook));
|
|
};
|
|
auto held_release = [&](bool isThrown) {
|
|
item_drop();
|
|
active.play(data.cursorIdle);
|
|
if (!isThrown && !actor.pending)
|
|
idle_play();
|
|
};
|
|
if (text.isScene && heldItem >= 0)
|
|
{
|
|
held_release(false);
|
|
isEatTextPending = false;
|
|
}
|
|
|
|
if (isEatTextPending && eatAnimation == actor.animation && (!actor.isPlaying || actor.is_finished()))
|
|
{
|
|
text.set(*this, data.dialogue.hook_get(isEatTextFull ? "EatFull" : "Eat"));
|
|
isEatTextPending = false;
|
|
eatAnimation = -1;
|
|
}
|
|
|
|
bool wasItemHovered = isItemHovered;
|
|
isItemHovered = false;
|
|
tooltipItem = -1;
|
|
if (spawnDelay > 0)
|
|
spawnDelay--;
|
|
for (int id : spawnQueue)
|
|
{
|
|
Rect spawn = areaIndex >= 0 ? area.spawn : bounds;
|
|
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]);
|
|
}
|
|
item_add(id, position);
|
|
progress.totalSpawned++;
|
|
particles_spawn(data.summonParticles, position);
|
|
}
|
|
spawnQueue.clear();
|
|
|
|
if (heldItem >= 0 && heldItem < (int)items.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;
|
|
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)
|
|
{
|
|
Rect rect = eat_rect_get(eatArea);
|
|
bool isInside = is_point_in_rect(rect, held.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();
|
|
if (isAreaActive || isPendingActive)
|
|
{
|
|
if (isPending && eatArea.event >= 0 && 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;
|
|
break;
|
|
}
|
|
eatItem = heldItem;
|
|
eatEvent = eatArea.event;
|
|
if (isAreaActive)
|
|
eatRect = rect;
|
|
if (!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");
|
|
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())
|
|
{
|
|
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();
|
|
}
|
|
|
|
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]);
|
|
if (isThrown)
|
|
text_play("Throw");
|
|
held.velocity = held.velocity - delta;
|
|
held_release(isThrown);
|
|
}
|
|
if (removed >= 0)
|
|
items.erase(items.begin() + removed);
|
|
}
|
|
|
|
int moved = -1;
|
|
bool isTargetLocked = pointer.target >= 0 && pointer.target < (int)items.size();
|
|
float right = bounds.x + bounds.w;
|
|
float bottom = bounds.y + bounds.h;
|
|
for (int i = 0; i < (int)items.size(); i++)
|
|
{
|
|
ItemEntity& entity = items[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);
|
|
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;
|
|
if (pointer.isClicked)
|
|
{
|
|
audio->play(data.sounds[SOUND_GRAB]);
|
|
particles_spawn(data.grabParticles, pointer.position);
|
|
isJustItemHeld = true;
|
|
}
|
|
if (pointer.isDown)
|
|
{
|
|
active.play(data.cursorGrab);
|
|
cursorState = CURSOR_ACTION;
|
|
heldItem = 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;
|
|
moved = moved == i ? -1 : moved > i ? moved - 1 : moved;
|
|
items.erase(items.begin() + i--);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (i != heldItem)
|
|
{
|
|
if (position.x <= bounds.x || position.x >= right)
|
|
{
|
|
position.x = position.x <= bounds.x ? bounds.x + 1 : right - 1;
|
|
velocity.x = -velocity.x * area.friction;
|
|
entity.angularVelocity = -entity.angularVelocity * area.friction;
|
|
rotation = -rotation;
|
|
audio->play(data.sounds[SOUND_BOUNCE]);
|
|
}
|
|
if (position.y <= bounds.y || position.y >= bottom)
|
|
{
|
|
if (position.y >= bottom && velocity.y <= gravity)
|
|
{
|
|
position.y = bottom;
|
|
velocity = {velocity.x * area.friction, 0};
|
|
rotation = std::remainder(rotation, 360.0f);
|
|
float step = ROTATION_RETURN_SPEED * ImGui::GetIO().DeltaTime;
|
|
rotation = std::abs(rotation) <= step ? 0 : rotation - std::copysign(step, rotation);
|
|
entity.angularVelocity *= ROTATION_GROUND_DAMPING;
|
|
}
|
|
else
|
|
{
|
|
velocity.y = -velocity.y;
|
|
entity.angularVelocity *= area.friction;
|
|
audio->play(data.sounds[SOUND_BOUNCE]);
|
|
}
|
|
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)};
|
|
}
|
|
|
|
if (moved >= 0 && moved < (int)items.size() - 1)
|
|
{
|
|
ItemEntity entity = std::move(items[moved]);
|
|
items.erase(items.begin() + moved);
|
|
items.push_back(std::move(entity));
|
|
heldItem = (int)items.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();
|
|
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;
|
|
}
|