#include "play.h" 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 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}); entity.actor.position = position; entity.actor.rotation = rotation; discovered[id] = true; } // Forgets which item is being eaten, and lets go of the held one unless asked to keep it. void Items::drop(Hold hold) { 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) { 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, catalog.rarities[item.rarity].weight); } bool Items::is_spawn_possible(Play& play) { Catalog& catalog = play.character->catalog; if ((int)list.size() >= catalog.spawnLimit) return false; for (int i = 0; i < (int)catalog.items.size(); i++) if (spawn_weight_get(play, i) > 0) return true; return false; } void Items::spawn_queue_add(Play& play, int id) { Catalog& catalog = play.character->catalog; if (id < 0 || id >= (int)catalog.items.size()) return; 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(play); } // Queues an item picked at random by rarity, once the spawn delay is up. void Items::spawn_queue_add(Play& play) { if (spawnDelay > 0 || !is_spawn_possible(play)) return; std::vector& items = play.character->catalog.items; float total = 0; 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)items.size(); i++) if (float weight = spawn_weight_get(play, i); weight > 0 && (roll -= weight) < 0) return spawn_queue_add(play, i); } void Items::update(Play& play, Pointer pointer) { Character& character = *play.character; Body& body = play.body; Stats& stats = body.stats; Actor& active = play.pointer_actor_get(); Audio& audio = *play.audio; Area& area = character.area; Rect bounds = play.bounds; bool isOver = body.is_over_capacity(); auto text_play = [&](Hook hook) { if (!pointer.isAutomated) play.text.set(play, character.dialogue.hook_get(hook)); }; auto release = [&](bool isThrown) { drop(); active.play(CURSOR_ANIMATION_IDLE); if (!isThrown && !body.actor.pending) body.idle_play(); }; if (play.text.isScene && held >= 0) { release(false); feeding.isTextPending = false; } if (feeding.isTextPending && feeding.animation == body.actor.animation && (!body.actor.isPlaying || body.actor.is_finished())) { play.text.set(play, character.dialogue.hook_get(feeding.isTextFull ? HOOK_EAT_FULL : HOOK_EAT)); feeding.isTextPending = false; feeding.animation = -1; } bool wasHovered = isHovered; isHovered = false; tooltip = -1; if (spawnDelay > 0) spawnDelay--; for (int id : spawnQueue) { Rect spawn = area.anm2 ? area.spawn : bounds; Vec2 position = {spawn.x + random_get(0, spawn.w), spawn.y + random_get(0, spawn.h)}; if (!discovered[id]) { play.toast_add("New item found!"); audio.play(character.sounds[SOUND_NEW_ITEM]); } add(play, id, position); stats.totalSpawned++; play.particles_spawn(character.catalog.summonParticles, position); } spawnQueue.clear(); if (held >= 0 && held < (int)list.size()) { 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 && 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 = body.eat_rect_get(eatArea); bool isInside = is_point_in_rect(rect, entity.actor.position); bool isAreaActive = isEdible && isInside; 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 && body.actor.event == eatArea.event) { 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.capacity, 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; } feeding.item = held; feeding.event = eatArea.event; if (isAreaActive) feeding.rect = rect; if (!body.actor.is_playing(eatName)) body.play(eatArea.animation, TRANSITION_NORMAL, INTERRUPT_ALWAYS); } 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(body.actor.bounds_get(), entity.actor.position) && character.alternate.texture && character.alternate.sheet < (int)body.actor.sheets.size()) { 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(character.sounds[isThrown ? SOUND_THROW : SOUND_RELEASE]); if (isThrown) text_play(HOOK_THROW); entity.velocity = entity.velocity - delta; release(isThrown); } if (removed >= 0) list.erase(list.begin() + removed); } int moved = -1; 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)list.size(); i++) { ItemEntity& entity = list[i]; Vec2& position = entity.actor.position; Vec2& velocity = entity.velocity; 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)) { isHovered = true; tooltip = held == i || pointer.isAutomated ? -1 : entity.id; active.play(CURSOR_ANIMATION_HOVER, TRANSITION_IF_IDLE); play.cursorState = CURSOR_HOVER; if (pointer.isClicked) { audio.play(character.sounds[SOUND_GRAB]); play.particles_spawn(character.cursor.grabParticles, pointer.position); isJustHeld = true; } if (pointer.isDown) { active.play(CURSOR_ANIMATION_GRAB); play.cursorState = CURSOR_ACTION; held = moved = i; } if (pointer.isRightClicked) { audio.play(character.sounds[SOUND_DISPOSE]); active.play(CURSOR_ANIMATION_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; list.erase(list.begin() + i--); continue; } } if (i != held) { 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(character.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 * TICK; 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(character.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)list.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 (wasHovered && !isHovered) active.play(CURSOR_ANIMATION_IDLE); pointerPrevious = pointer.position; }