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
@@ -0,0 +1,289 @@
|
||||
#include "play.h"
|
||||
|
||||
constexpr float AUTO_ACCELERATION = 0.45f;
|
||||
constexpr float AUTO_ANGLE_STEP = 0.08f;
|
||||
constexpr float AUTO_BRAKING = 2;
|
||||
constexpr float AUTO_CALORIE_EPSILON = 0.001f;
|
||||
constexpr float AUTO_DIGEST_ACCELERATION = 0.15f;
|
||||
constexpr float AUTO_DIGEST_RADIUS = 0.25f;
|
||||
constexpr float AUTO_DIGEST_SPEED = 3;
|
||||
constexpr float AUTO_SCALE_AREA = 512.0f * 512.0f;
|
||||
constexpr float AUTO_SCALE_MAX = 2;
|
||||
constexpr float AUTO_SCALE_MIN = 0.5f;
|
||||
constexpr float AUTO_SPEED = 9;
|
||||
constexpr float AUTO_TARGET_EPSILON = 0.001f;
|
||||
constexpr int AUTO_CLICK_COOLDOWN = 45;
|
||||
|
||||
void Interactions::update(Play& play, Pointer pointer)
|
||||
{
|
||||
Character& character = *play.character;
|
||||
Body& body = play.body;
|
||||
Text& text = play.text;
|
||||
Actor& active = play.pointer_actor_get();
|
||||
if (text.isScene)
|
||||
{
|
||||
isInteracting = isHolding = isHovering = false;
|
||||
area = -1;
|
||||
return;
|
||||
}
|
||||
bool wasInteracting = isInteracting;
|
||||
bool wasHolding = isHolding;
|
||||
bool wasHovering = isHovering;
|
||||
isHovering = false;
|
||||
isHolding &= isInteracting;
|
||||
cooldowns.resize(character.interactAreas.size());
|
||||
for (int& cooldown : cooldowns)
|
||||
cooldown = text.entry ? std::max(0, cooldown - 1) : 0;
|
||||
|
||||
for (int i = 0; i < (int)character.interactAreas.size(); i++)
|
||||
{
|
||||
InteractArea& entry = character.interactAreas[i];
|
||||
bool isInside = is_point_in_rect(body.actor.null_rect_get(entry.null), pointer.position);
|
||||
if (play.cursorState == CURSOR_DEFAULT && isInside && !pointer.isCaptured && entry.type == pointer.type)
|
||||
{
|
||||
play.cursorState = CURSOR_HOVER;
|
||||
active.play(entry.cursorHover, TRANSITION_IF_IDLE);
|
||||
isHovering = true;
|
||||
area = i;
|
||||
bool isHoldStart = entry.isHold && (pointer.isClicked || (pointer.isDown && !isInteracting));
|
||||
if (isHoldStart || (!entry.isHold && pointer.isClicked))
|
||||
{
|
||||
if (entry.isHold)
|
||||
isInteracting = isHolding = true;
|
||||
else
|
||||
{
|
||||
play.cursorState = CURSOR_ACTION;
|
||||
active.play(entry.cursorActive, TRANSITION_FORCE, INTERRUPT_ALWAYS);
|
||||
}
|
||||
play.audio->play(entry.sounds);
|
||||
body.idle_override_set(entry.idleOnEnd);
|
||||
play.particles_spawn(entry.particles, pointer.position);
|
||||
if (entry.digestionClick > 0 && body.stats.calories > 0 && !body.stats.isDigesting)
|
||||
body.stats.digestionProgress += entry.digestionClick;
|
||||
body.actor.pulse_add(entry.layer, entry.scaleAmplitude, entry.scaleCycles, entry.scaleTime);
|
||||
bool isReady = cooldowns[i] <= 0;
|
||||
cooldowns[i] = std::max(0, entry.dialogueCooldown);
|
||||
Dialogue& dialogue = character.dialogue;
|
||||
bool isFull = body.is_over_capacity() && dialogue.pools.contains(entry.dialoguePoolFull);
|
||||
const std::string& pool = isFull ? entry.dialoguePoolFull : entry.dialoguePool;
|
||||
if (dialogue.pools.contains(pool))
|
||||
{
|
||||
if (text.is_interruptible() && isReady)
|
||||
text.set(play, dialogue.pool_get(pool), TEXT_INTERRUPTIBLE, entry.isHold ? "" : entry.idleOnEnd);
|
||||
}
|
||||
else if (entry.dialoguePool.empty() && !entry.animation.empty())
|
||||
body.play(entry.animation, TRANSITION_FORCE);
|
||||
}
|
||||
if (isInteracting)
|
||||
{
|
||||
play.cursorState = CURSOR_ACTION;
|
||||
active.play(entry.cursorActive);
|
||||
Vec2 moved = pointer.position - pointerPrevious;
|
||||
if (entry.digestionHover > 0 && body.stats.calories > 0 && !body.stats.isDigesting &&
|
||||
(moved.x != 0 || moved.y != 0))
|
||||
body.stats.digestionProgress += entry.digestionHover;
|
||||
}
|
||||
}
|
||||
if ((i == area && !isInside) || pointer.isReleased || pointer.isCaptured)
|
||||
{
|
||||
isInteracting = isHolding = false;
|
||||
area = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if ((wasInteracting && !isInteracting) || (wasHovering && !isHovering))
|
||||
active.play(character.cursor.idle);
|
||||
if (wasHolding && !isHolding && !body.actor.pending)
|
||||
body.idle_play();
|
||||
pointerPrevious = pointer.position;
|
||||
}
|
||||
|
||||
// Moves the cursor toward a target, braking to land on it. Returns whether it's there.
|
||||
static bool is_target_reached(Vec2& position, Vec2& velocity, Vec2 target, float speed, float acceleration)
|
||||
{
|
||||
Vec2 delta = target - position;
|
||||
float distance = vec2_length(delta);
|
||||
if (!std::isfinite(distance))
|
||||
return false;
|
||||
if (distance <= AUTO_TARGET_EPSILON)
|
||||
{
|
||||
position = target;
|
||||
velocity = {};
|
||||
return true;
|
||||
}
|
||||
Vec2 desired = delta / distance * std::min(speed, std::sqrt(AUTO_BRAKING * acceleration * distance));
|
||||
Vec2 change = desired - velocity;
|
||||
float changeLength = vec2_length(change);
|
||||
velocity = changeLength > acceleration ? velocity + change / changeLength * acceleration : desired;
|
||||
if (vec2_length(velocity) >= distance)
|
||||
{
|
||||
position = target;
|
||||
velocity = {};
|
||||
return true;
|
||||
}
|
||||
position = position + velocity;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_autofeed_food(const Item& item)
|
||||
{
|
||||
return item.use == USE_EDIBLE && item.capacityBonus >= 0 && item.digestionBonus >= 0;
|
||||
}
|
||||
|
||||
static bool is_edible(Play& play, const Item& item)
|
||||
{
|
||||
return is_autofeed_food(item) && item.calories > 0 &&
|
||||
play.body.stats.calories + item.calories <= play.body.capacity_max_get();
|
||||
}
|
||||
|
||||
// The edible item in the world with the most calories, ties broken at random.
|
||||
static int food_best_get(Play& play)
|
||||
{
|
||||
std::vector<Item>& catalog = play.character->catalog.items;
|
||||
std::vector<ItemEntity>& list = play.items.list;
|
||||
int best = -1;
|
||||
int ties = 0;
|
||||
for (int i = 0; i < (int)list.size(); i++)
|
||||
{
|
||||
Item& item = catalog[list[i].id];
|
||||
if (!is_edible(play, item))
|
||||
continue;
|
||||
float bestCalories = best >= 0 ? catalog[list[best].id].calories : -1;
|
||||
if (item.calories > bestCalories)
|
||||
best = i, ties = 1;
|
||||
else if (item.calories == bestCalories && SDL_rand(++ties) == 0)
|
||||
best = i;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// The first interact area that helps digestion, which the autofeed rubs or pats.
|
||||
static InteractArea* digest_area_get(Play& play)
|
||||
{
|
||||
for (InteractArea& area : play.character->interactAreas)
|
||||
if (area.digestionClick > 0 || area.digestionHover > 0)
|
||||
return &area;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int Autofeed::type_get(Play& play)
|
||||
{
|
||||
InteractArea* area = digest_area_get(play);
|
||||
return area ? area->type : play.cursorType;
|
||||
}
|
||||
|
||||
bool Autofeed::is_available(Play& play)
|
||||
{
|
||||
bool isFood = play.body.stats.calories > 0 || play.items.is_spawn_possible(play);
|
||||
for (ItemEntity& entity : play.items.list)
|
||||
isFood |= play.character->catalog.items[entity.id].use == USE_EDIBLE;
|
||||
return isFood;
|
||||
}
|
||||
|
||||
void Autofeed::reset()
|
||||
{
|
||||
isItemHeld = isDigestHeld = false;
|
||||
target = -1;
|
||||
clickCooldown = 0;
|
||||
velocity = {};
|
||||
}
|
||||
|
||||
void Autofeed::update(Play& play, Pointer& itemPointer, Pointer& interactPointer)
|
||||
{
|
||||
Body& body = play.body;
|
||||
Items& items = play.items;
|
||||
Actor& cursor = play.autoCursor;
|
||||
std::vector<Item>& catalog = play.character->catalog.items;
|
||||
cursor.isVisible = false;
|
||||
bool isWorldFood = false;
|
||||
bool isAnyEdible = false;
|
||||
for (ItemEntity& entity : items.list)
|
||||
{
|
||||
isWorldFood |= is_autofeed_food(catalog[entity.id]);
|
||||
isAnyEdible |= is_edible(play, catalog[entity.id]);
|
||||
}
|
||||
if (!isEnabled || body.isStageUp)
|
||||
return reset();
|
||||
items.spawn_queue_add(play);
|
||||
if (body.stats.calories <= AUTO_CALORIE_EPSILON && !isWorldFood)
|
||||
return reset();
|
||||
|
||||
if (cursor.position.x == 0 && cursor.position.y == 0)
|
||||
cursor.position = body.actor.position;
|
||||
Rect bodyBounds = body.actor.bounds_get();
|
||||
float speedScale = is_rect_valid(bodyBounds) ? std::clamp(std::sqrt(bodyBounds.w * bodyBounds.h / AUTO_SCALE_AREA),
|
||||
AUTO_SCALE_MIN, AUTO_SCALE_MAX)
|
||||
: 1;
|
||||
Rect mouth = {NAN, NAN, NAN, NAN};
|
||||
for (EatArea& eatArea : play.character->eatAreas)
|
||||
if (!is_rect_valid(mouth))
|
||||
mouth = body.eat_rect_get(eatArea);
|
||||
bool isMouth = is_rect_valid(mouth);
|
||||
auto move = [&](Vec2 destination, float speed, float acceleration) {
|
||||
isActive = cursor.isVisible = true;
|
||||
itemPointer = interactPointer = {.position = cursor.position, .isAutomated = true};
|
||||
bool isThere =
|
||||
is_target_reached(cursor.position, velocity, destination, speed * speedScale, acceleration * speedScale);
|
||||
itemPointer.position = interactPointer.position = cursor.position;
|
||||
return isThere;
|
||||
};
|
||||
|
||||
if (items.held >= 0 && items.held < (int)items.list.size())
|
||||
{
|
||||
bool isCarried = isMouth && is_edible(play, catalog[items.list[items.held].id]);
|
||||
move(isCarried ? rect_middle(mouth) : cursor.position, AUTO_SPEED, AUTO_ACCELERATION);
|
||||
itemPointer.isDown = isItemHeld = isCarried;
|
||||
itemPointer.isReleased = !isCarried;
|
||||
isDigestHeld = false;
|
||||
target = isCarried ? target : -1;
|
||||
return;
|
||||
}
|
||||
|
||||
bool isTargetValid = target >= 0 && target < (int)items.list.size() && is_edible(play, catalog[items.list[target].id]);
|
||||
if (!isMouth || !isTargetValid)
|
||||
target = isMouth ? food_best_get(play) : -1;
|
||||
if (target >= 0)
|
||||
{
|
||||
Rect rect = items.list[target].actor.bounds_get();
|
||||
if (!is_rect_valid(rect))
|
||||
return reset();
|
||||
move(rect_middle(rect), AUTO_SPEED, AUTO_ACCELERATION);
|
||||
bool isInside = is_point_in_rect(rect, cursor.position);
|
||||
itemPointer.target = target;
|
||||
itemPointer.isClicked = isInside && !isItemHeld;
|
||||
itemPointer.isDown = isInside || isItemHeld;
|
||||
isItemHeld |= isInside;
|
||||
isDigestHeld = false;
|
||||
return;
|
||||
}
|
||||
if (!isWorldFood && isAnyEdible)
|
||||
return reset();
|
||||
|
||||
InteractArea* area = digest_area_get(play);
|
||||
Rect rect = area ? body.actor.null_rect_get(area->null) : Rect{NAN, NAN, NAN, NAN};
|
||||
if (!is_rect_valid(rect))
|
||||
return reset();
|
||||
angle += AUTO_ANGLE_STEP;
|
||||
float radius = std::max(1.0f, std::min(rect.w, rect.h) * AUTO_DIGEST_RADIUS);
|
||||
Vec2 destination = rect_middle(rect) + Vec2{std::cos(angle), std::sin(angle)} * radius;
|
||||
bool isThere = move(destination, AUTO_DIGEST_SPEED, AUTO_DIGEST_ACCELERATION);
|
||||
interactPointer.type = area->type;
|
||||
if (area->digestionHover > 0)
|
||||
{
|
||||
bool isInside = is_point_in_rect(rect, cursor.position);
|
||||
interactPointer.isClicked = isInside && !isDigestHeld;
|
||||
interactPointer.isDown = isInside;
|
||||
interactPointer.isReleased = isDigestHeld && !isInside;
|
||||
isDigestHeld = isInside;
|
||||
}
|
||||
else
|
||||
{
|
||||
clickCooldown = std::max(0, clickCooldown - 1);
|
||||
interactPointer.isClicked = isThere && clickCooldown <= 0 && !isDigestHeld;
|
||||
interactPointer.isReleased = isDigestHeld;
|
||||
if (interactPointer.isClicked)
|
||||
clickCooldown = AUTO_CLICK_COOLDOWN;
|
||||
isDigestHeld = interactPointer.isClicked;
|
||||
}
|
||||
isItemHeld = false;
|
||||
}
|
||||
Reference in new issue
Block a user