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
+251
-463
@@ -4,10 +4,13 @@
|
||||
#include <utility>
|
||||
|
||||
constexpr Rect WORLD_BOUNDS = {96, 54, 1728, 972};
|
||||
constexpr ImGuiKey CHEAT_CODE[] = {ImGuiKey_UpArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_DownArrow,
|
||||
ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow,
|
||||
ImGuiKey_B, ImGuiKey_A};
|
||||
constexpr const char* AUTOFEED_TYPE = "Rub";
|
||||
constexpr ImGuiKey CHEAT_CODE[] = {ImGuiKey_UpArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_DownArrow,
|
||||
ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow,
|
||||
ImGuiKey_B, ImGuiKey_A};
|
||||
constexpr ImGuiKey CHEAT_KEYS[] = {ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_LeftArrow,
|
||||
ImGuiKey_RightArrow, ImGuiKey_B, ImGuiKey_A};
|
||||
constexpr const char* KEY_DISCOVERED = "discovered";
|
||||
constexpr const char* KEY_ITEM = "item";
|
||||
constexpr float AUTOSAVE_TIME = 30;
|
||||
constexpr float CHEAT_TIME = 5;
|
||||
constexpr float FOCUS_MENU_WIDTH = 0.3f;
|
||||
@@ -17,18 +20,14 @@ constexpr float SHAKE_DECAY = 0.97f;
|
||||
constexpr float SHAKE_EPSILON = 0.1f;
|
||||
constexpr float SHAKE_LERP = 0.72f;
|
||||
constexpr float SHAKE_MAGNITUDE = 0.1f;
|
||||
constexpr float TALK_STEP = 0.5f;
|
||||
constexpr int OVERRIDE_TALK = 0;
|
||||
constexpr int OVERRIDE_BLINK = 1;
|
||||
constexpr int OVERRIDE_EXPAND = 2;
|
||||
|
||||
// Keeps a value within a range given either way round, putting what isn't a number at its low end.
|
||||
static float value_clamp(float value, float low, float high)
|
||||
struct SavedItem
|
||||
{
|
||||
if (low > high)
|
||||
std::swap(low, high);
|
||||
return std::isfinite(value) ? std::clamp(value, low, high) : low;
|
||||
}
|
||||
std::string name;
|
||||
Vec2 position;
|
||||
Vec2 velocity;
|
||||
float rotation = 0;
|
||||
};
|
||||
|
||||
static int zoom_index_get(float zoom)
|
||||
{
|
||||
@@ -39,116 +38,99 @@ static int zoom_index_get(float zoom)
|
||||
return index;
|
||||
}
|
||||
|
||||
// Starts a character, from its save unless it's a new game. A save is the body's stats, the play's own fields (see
|
||||
// visit), the items found (one "discovered" line each) and those lying in the world ("item" lines of position,
|
||||
// velocity, rotation and name).
|
||||
void Play::set(Character& played, Game game, Renderer& renderer, Audio& sound)
|
||||
{
|
||||
played.load(renderer, sound);
|
||||
*this = Play();
|
||||
character = &played;
|
||||
audio = &sound;
|
||||
camera.zoom = 0;
|
||||
|
||||
std::vector<std::string> found;
|
||||
std::vector<SavedItem> saved;
|
||||
if (game == GAME_CONTINUE)
|
||||
progress.load(played.save_path_get());
|
||||
if (!progress.isValid)
|
||||
{
|
||||
progress.weight = played.weight;
|
||||
progress.capacity = played.capacity;
|
||||
progress.digestionRate = played.digestionRate;
|
||||
}
|
||||
is_file_read(played.save_path_get(), [&](const std::string& key, const char* value) {
|
||||
field_line_read(body.stats, key, value);
|
||||
field_line_read(*this, key, value);
|
||||
SavedItem item;
|
||||
int length = 0;
|
||||
if (key == KEY_DISCOVERED)
|
||||
found.emplace_back(value);
|
||||
else if (key == KEY_ITEM && std::sscanf(value, "%f %f %f %f %f %n", &item.position.x, &item.position.y,
|
||||
&item.velocity.x, &item.velocity.y, &item.rotation, &length) == 5)
|
||||
saved.push_back({value + length, item.position, item.velocity, item.rotation});
|
||||
});
|
||||
body.set(played, sound, game);
|
||||
area_set();
|
||||
|
||||
actor = Actor(played.anm2);
|
||||
actor.overrides.resize(OVERRIDE_EXPAND);
|
||||
actor.overrides[OVERRIDE_TALK].id = played.talkDestination;
|
||||
actor.overrides[OVERRIDE_BLINK].id = played.blinkDestination;
|
||||
for (ExpandArea& expandArea : played.expandAreas)
|
||||
Catalog& catalog = played.catalog;
|
||||
items.discovered.assign(catalog.items.size(), false);
|
||||
for (const std::string& name : found)
|
||||
if (int id = catalog.item_get(name); id >= 0)
|
||||
items.discovered[id] = true;
|
||||
for (SavedItem& item : saved)
|
||||
{
|
||||
actor.overrides.push_back({.id = expandArea.layer});
|
||||
actor.overrides.push_back({.id = expandArea.null, .target = TARGET_NULL});
|
||||
int id = catalog.item_get(item.name);
|
||||
if (id >= 0)
|
||||
items.add(*this, id, item.position, item.velocity, item.rotation);
|
||||
else
|
||||
SDL_Log("Saved item not found: %s", item.name.c_str());
|
||||
}
|
||||
values_clamp();
|
||||
stage = stage_get(progress.weight);
|
||||
|
||||
bool isRolled = game == GAME_NEW && played.alternateTexture && is_chance(played.alternateChance);
|
||||
progress.isAlternate |= isRolled;
|
||||
if (isRolled)
|
||||
audio->play(played.alternateSound);
|
||||
if (progress.isAlternate && played.alternateSheet < (int)actor.sheets.size())
|
||||
actor.sheets[played.alternateSheet] = played.alternateTexture;
|
||||
|
||||
cursorType = played.interactTypes.empty() ? -1 : 0;
|
||||
if (played.cursor)
|
||||
if (played.cursor.anm2)
|
||||
{
|
||||
cursor = Actor(played.cursor);
|
||||
autoCursor = Actor(played.cursor);
|
||||
cursor = Actor(played.cursor.anm2);
|
||||
autoCursor = Actor(played.cursor.anm2);
|
||||
autoCursor.isVisible = false;
|
||||
}
|
||||
|
||||
spawnDelay = progress.spawnDelay;
|
||||
discovered.assign(played.items.size(), false);
|
||||
auto item_get = [&](const std::string& name) {
|
||||
for (int i = 0; i < (int)played.items.size(); i++)
|
||||
if (played.items[i].name == name)
|
||||
return i;
|
||||
return -1;
|
||||
};
|
||||
for (const std::string& name : progress.discovered)
|
||||
if (int id = item_get(name); id >= 0)
|
||||
discovered[id] = true;
|
||||
for (SavedItem& saved : progress.items)
|
||||
if (int id = item_get(saved.name); id >= 0)
|
||||
item_add(id, saved.position, saved.velocity, saved.rotation);
|
||||
|
||||
isPostgame = progress.isPostgame || stage >= (int)played.stages.size();
|
||||
isPostgame |= body.stage >= (int)played.stages.size();
|
||||
#ifdef DEBUG
|
||||
isCheats = true;
|
||||
#endif
|
||||
isCheats |= isPostgame;
|
||||
|
||||
int areaIndex = played.area_get(stage);
|
||||
if (areaIndex >= 0)
|
||||
area = Actor(played.areas[areaIndex].anm2);
|
||||
bounds = areaIndex >= 0 && is_rect_valid(played.areas[areaIndex].bounds) ? played.areas[areaIndex].bounds
|
||||
: WORLD_BOUNDS;
|
||||
idle_play();
|
||||
actor.update(*audio);
|
||||
isFocusPending = true;
|
||||
camera.isFocusPending = camera.zoom < ZOOM_LADDER[0];
|
||||
camera.zoom = camera.isFocusPending ? ZOOM_LADDER[0] : ZOOM_LADDER[zoom_index_get(camera.zoom)];
|
||||
if (game == GAME_NEW)
|
||||
text.set(*this, played.dialogue.hook_get("Start"), TEXT_UNINTERRUPTIBLE);
|
||||
text.set(*this, played.dialogue.hook_get(HOOK_START), TEXT_UNINTERRUPTIBLE);
|
||||
}
|
||||
|
||||
void Play::save()
|
||||
{
|
||||
if (!character)
|
||||
return;
|
||||
progress.pan = pan;
|
||||
progress.zoom = zoom;
|
||||
progress.spawnDelay = spawnDelay;
|
||||
progress.isPostgame = isPostgame;
|
||||
progress.items.clear();
|
||||
for (ItemEntity& entity : items)
|
||||
progress.items.push_back(
|
||||
{character->items[entity.id].name, entity.actor.position, entity.velocity, entity.rotation_get()});
|
||||
progress.discovered.clear();
|
||||
for (int i = 0; i < (int)discovered.size(); i++)
|
||||
if (discovered[i])
|
||||
progress.discovered.push_back(character->items[i].name);
|
||||
progress.save(character->save_path_get());
|
||||
toast_add(STRING_ToastSaving);
|
||||
std::string text = fields_write(body.stats) + fields_write(*this);
|
||||
for (int i = 0; i < (int)items.discovered.size(); i++)
|
||||
if (items.discovered[i])
|
||||
text += std::string(KEY_DISCOVERED) + "=" + character->catalog.items[i].name + "\n";
|
||||
for (ItemEntity& entity : items.list)
|
||||
text += std::string(KEY_ITEM) + "=" + field_write(entity.actor.position) + " " + field_write(entity.velocity) +
|
||||
" " + field_write(entity.actor.rotation.value_or(0)) + " " + character->catalog.items[entity.id].name +
|
||||
"\n";
|
||||
file_write(character->save_path_get(), text);
|
||||
toast_add("Saving...");
|
||||
}
|
||||
|
||||
void Play::toast_add(StringId message)
|
||||
// Shows the area of the body's stage, with its bounds.
|
||||
void Play::area_set()
|
||||
{
|
||||
toasts.push_back({character->strings[message], TOAST_LIFE});
|
||||
int index = character->area_get(body.stage);
|
||||
if (index >= 0 && area.anm2 != character->areas[index].anm2)
|
||||
area = Actor(character->areas[index].anm2);
|
||||
bounds = index >= 0 && is_rect_valid(character->areas[index].bounds) ? character->areas[index].bounds : WORLD_BOUNDS;
|
||||
}
|
||||
|
||||
void Play::shake_add(float magnitude, int time)
|
||||
void Play::toast_add(const std::string& message)
|
||||
{
|
||||
shakeTimeMax = std::max(1, time);
|
||||
shakeTime = shakeTimeMax;
|
||||
shake = {random_get(-magnitude, magnitude), random_get(-magnitude, magnitude)};
|
||||
toasts.push_back({message, TOAST_LIFE});
|
||||
}
|
||||
|
||||
Vec2 Play::world_get(Vec2 screen)
|
||||
Actor& Play::pointer_actor_get()
|
||||
{
|
||||
return pan + screen / (zoom / PERCENT);
|
||||
return autofeed.isActive ? autoCursor : cursor;
|
||||
}
|
||||
|
||||
Focus Play::focus_get()
|
||||
@@ -158,254 +140,209 @@ Focus Play::focus_get()
|
||||
return isMenuOpen && isToolsOpen ? FOCUS_BOTH : isMenuOpen ? FOCUS_MENU : isToolsOpen ? FOCUS_TOOLS : FOCUS_CENTER;
|
||||
}
|
||||
|
||||
// Zooms and pans so the character fills the view, leaving room for the open panels.
|
||||
void Play::focus(Vec2 screenSize)
|
||||
{
|
||||
Rect rect = actor.bounds_get();
|
||||
if (!is_rect_valid(rect))
|
||||
camera.focus(body.actor.bounds_get(), screenSize, focus_get());
|
||||
}
|
||||
|
||||
void Play::particles_spawn(const std::vector<std::string>& names, Vec2 position)
|
||||
{
|
||||
for (const std::string& name : names)
|
||||
{
|
||||
auto found = character->particles.find(name);
|
||||
if (found == character->particles.end() || !found->second.anm2)
|
||||
SDL_Log("Particle not found: %s", name.c_str());
|
||||
else
|
||||
particles.emplace_back(found->second.anm2, position, found->second.animation);
|
||||
}
|
||||
}
|
||||
|
||||
// The Konami code unlocks the cheats menu, keyed within a few seconds of each press.
|
||||
void Play::cheats_update()
|
||||
{
|
||||
if (isCheats)
|
||||
return;
|
||||
rect = rect_center(rect, FOCUS_PADDING);
|
||||
float factor = std::min(screenSize.x / rect.w, screenSize.y / rect.h);
|
||||
for (ImGuiKey key : CHEAT_KEYS)
|
||||
{
|
||||
if (!ImGui::IsKeyPressed(key, false))
|
||||
continue;
|
||||
cheatIndex = key == CHEAT_CODE[cheatIndex] ? cheatIndex + 1 : key == CHEAT_CODE[0] ? 1 : 0;
|
||||
cheatTime = (float)ImGui::GetTime();
|
||||
if (cheatIndex < (int)std::size(CHEAT_CODE))
|
||||
continue;
|
||||
isCheats = true;
|
||||
cheatIndex = 0;
|
||||
toast_add("Cheats unlocked!");
|
||||
audio->play(character->sounds[SOUND_CHEATS]);
|
||||
}
|
||||
if (cheatIndex > 0 && ImGui::GetTime() - cheatTime > CHEAT_TIME)
|
||||
cheatIndex = 0;
|
||||
}
|
||||
|
||||
// Answers what the body did this tick: dialogue for digesting and stage ups, and the ending at the final stage.
|
||||
void Play::events_handle(Vec2 screenSize)
|
||||
{
|
||||
Dialogue& dialogue = character->dialogue;
|
||||
for (Event event : events)
|
||||
{
|
||||
if (event == EVENT_DIGESTED && !text.isScene && text.is_interruptible())
|
||||
text.set(*this, dialogue.hook_get(HOOK_DIGEST));
|
||||
if (event == EVENT_STAGE_UP && !text.isScene)
|
||||
text.set(*this, dialogue.hook_get(HOOK_STAGE_UP));
|
||||
if (event != EVENT_STAGE_FINAL || isPostgame || isPostgamePending)
|
||||
continue;
|
||||
isPostgame = isCheats = !dialogue.is_hook(HOOK_END);
|
||||
if (isPostgame)
|
||||
continue;
|
||||
text.set(*this, dialogue.hook_get(HOOK_END), TEXT_UNINTERRUPTIBLE);
|
||||
isMenuOpen = isToolsOpen = false;
|
||||
body.stats.calories = 0;
|
||||
body.stats.digestionProgress = 0;
|
||||
items.drop();
|
||||
isPostgamePending = true;
|
||||
focus(screenSize);
|
||||
}
|
||||
events.clear();
|
||||
}
|
||||
|
||||
void Play::update(Vec2 screenSize)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (std::exchange(camera.isFocusPending, false))
|
||||
focus(screenSize);
|
||||
body.stats.playtime += TICK;
|
||||
cheats_update();
|
||||
text.update(*this);
|
||||
if (isPostgamePending && (!text.entry || (text.entry->isTerminal && text.isFinished)) && !text.isScene)
|
||||
{
|
||||
isMenuOpen = isPostgame = isCheats = true;
|
||||
isPostgamePending = false;
|
||||
focus(screenSize);
|
||||
}
|
||||
|
||||
if (autofeed.isEnabled)
|
||||
cursorType = autofeed.type_get(*this);
|
||||
Pointer pointer;
|
||||
pointer.position = camera.world_get({io.MousePos.x, io.MousePos.y});
|
||||
pointer.type = cursorType;
|
||||
pointer.isCaptured = io.WantCaptureMouse;
|
||||
pointer.isClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
|
||||
pointer.isDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
|
||||
pointer.isReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
||||
pointer.isRightClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
|
||||
Pointer itemPointer = pointer;
|
||||
Pointer interactPointer = pointer;
|
||||
autofeed.isActive = false;
|
||||
if (text.isScene)
|
||||
itemPointer = interactPointer = {.position = pointer.position, .isCaptured = true};
|
||||
else
|
||||
autofeed.update(*this, itemPointer, interactPointer);
|
||||
items.update(*this, itemPointer);
|
||||
interactions.update(*this, interactPointer);
|
||||
|
||||
if (cursor.anm2 && !autofeed.isActive && pointer.isClicked && !items.isHovered && !interactions.isHovering &&
|
||||
cursorState == CURSOR_DEFAULT)
|
||||
cursor.play(character->cursor.click, TRANSITION_FORCE, INTERRUPT_ALWAYS);
|
||||
|
||||
body.update(events);
|
||||
events_handle(screenSize);
|
||||
area_set();
|
||||
area.update(*audio);
|
||||
|
||||
for (int i = 0; i < (int)particles.size(); i++)
|
||||
{
|
||||
particles[i].update(*audio);
|
||||
if (!particles[i].isPlaying)
|
||||
particles.erase(particles.begin() + i--);
|
||||
}
|
||||
for (Actor* each : {&cursor, &autoCursor})
|
||||
{
|
||||
bool isActive = (each == &autoCursor) == autofeed.isActive;
|
||||
if ((!isActive || cursorState == CURSOR_DEFAULT) && !each->pending)
|
||||
each->play(character->cursor.idle, TRANSITION_IF_IDLE);
|
||||
each->update(*audio);
|
||||
}
|
||||
cursorState = CURSOR_DEFAULT;
|
||||
cursor.position = {io.MousePos.x, io.MousePos.y};
|
||||
camera.update(*this, screenSize);
|
||||
|
||||
for (int i = 0; i < (int)toasts.size(); i++)
|
||||
if (--toasts[i].life <= 0)
|
||||
toasts.erase(toasts.begin() + i--);
|
||||
if ((autosaveTime += TICK) > AUTOSAVE_TIME)
|
||||
{
|
||||
save();
|
||||
autosaveTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Play::render(Renderer& renderer, Vec2 screenSize)
|
||||
{
|
||||
Affine view = camera.view_get(screenSize);
|
||||
area.render(renderer, view);
|
||||
body.actor.render(renderer, view);
|
||||
if (!text.isScene)
|
||||
for (ItemEntity& entity : items.list)
|
||||
entity.actor.render(renderer, view);
|
||||
if (autofeed.isActive)
|
||||
autoCursor.render(renderer, view);
|
||||
for (Actor& particle : particles)
|
||||
particle.render(renderer, view);
|
||||
}
|
||||
|
||||
Vec2 Camera::world_get(Vec2 screen)
|
||||
{
|
||||
return pan + screen / (zoom / PERCENT);
|
||||
}
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
void Camera::shake_add(float magnitude, int time)
|
||||
{
|
||||
shakeTimeMax = std::max(1, time);
|
||||
shakeTime = shakeTimeMax;
|
||||
shake = {random_get(-magnitude, magnitude), random_get(-magnitude, magnitude)};
|
||||
}
|
||||
|
||||
// Zooms and pans so a target fills the view, leaving room for the open panels.
|
||||
void Camera::focus(Rect target, Vec2 screenSize, Focus panels)
|
||||
{
|
||||
if (!is_rect_valid(target))
|
||||
return;
|
||||
target = rect_center(target, FOCUS_PADDING);
|
||||
float factor = std::min(screenSize.x / target.w, screenSize.y / target.h);
|
||||
zoom = ZOOM_LADDER[zoom_index_get(std::clamp(factor * PERCENT, ZOOM_LADDER[0], std::end(ZOOM_LADDER)[-1]))];
|
||||
factor = zoom / PERCENT;
|
||||
pan = rect_middle(rect) - screenSize / factor / 2;
|
||||
Focus panels = focus_get();
|
||||
pan = rect_middle(target) - screenSize / factor / 2;
|
||||
if (panels == FOCUS_MENU || panels == FOCUS_BOTH)
|
||||
pan.x += screenSize.x * FOCUS_MENU_WIDTH / factor / 2;
|
||||
if (panels == FOCUS_TOOLS || panels == FOCUS_BOTH)
|
||||
pan.x -= screenSize.x * FOCUS_TOOLS_WIDTH / factor / 2;
|
||||
}
|
||||
|
||||
float Play::capacity_max_get()
|
||||
{
|
||||
return progress.capacity * character->overstuffedMultiplier;
|
||||
}
|
||||
|
||||
float Play::capacity_percent_get()
|
||||
{
|
||||
float capacityMax = capacity_max_get();
|
||||
return capacityMax > 0 ? progress.calories / capacityMax : 0;
|
||||
}
|
||||
|
||||
bool Play::is_over_capacity()
|
||||
{
|
||||
return progress.calories > progress.capacity;
|
||||
}
|
||||
|
||||
int Play::stage_get(float weight)
|
||||
{
|
||||
std::vector<Stage>& stages = character->stages;
|
||||
if (stages.empty() || weight <= character->weight)
|
||||
return 0;
|
||||
for (int i = 0; i < (int)stages.size(); i++)
|
||||
if (weight < stages[i].weight)
|
||||
return i;
|
||||
return (int)stages.size();
|
||||
}
|
||||
|
||||
float Play::stage_threshold_get(int index)
|
||||
{
|
||||
std::vector<Stage>& stages = character->stages;
|
||||
if (index <= 0 || stages.empty())
|
||||
return character->weight;
|
||||
return stages[std::min(index, (int)stages.size()) - 1].weight;
|
||||
}
|
||||
|
||||
float Play::stage_progress_get()
|
||||
{
|
||||
float low = stage_threshold_get(stage);
|
||||
float high = stage_threshold_get(stage + 1);
|
||||
if (stage >= (int)character->stages.size() || high <= low)
|
||||
return 1;
|
||||
return (progress.weight - low) / (high - low);
|
||||
}
|
||||
|
||||
std::string Play::suffix_get()
|
||||
{
|
||||
return stage > 0 && stage <= (int)character->stages.size() ? character->stages[stage - 1].animationSuffix : "";
|
||||
}
|
||||
|
||||
std::string Play::idle_get()
|
||||
{
|
||||
if (isStageUp)
|
||||
return "";
|
||||
if (!idleOverride.empty())
|
||||
return idleOverride;
|
||||
return is_over_capacity() && !character->animationIdleFull.empty() ? character->animationIdleFull
|
||||
: character->animationIdle;
|
||||
}
|
||||
|
||||
// Plays one of the character's animations, as its current stage draws it.
|
||||
void Play::body_play(const std::string& animation, Transition transition, Interrupt interrupt)
|
||||
{
|
||||
if (!animation.empty())
|
||||
actor.play(animation + suffix_get(), transition, interrupt);
|
||||
}
|
||||
|
||||
void Play::idle_play()
|
||||
{
|
||||
body_play(idle_get());
|
||||
}
|
||||
|
||||
void Play::idle_override_set(const std::string& animation)
|
||||
{
|
||||
if (!animation.empty())
|
||||
idleOverride = animation == character->animationIdle ? "" : animation;
|
||||
}
|
||||
|
||||
void Play::values_clamp()
|
||||
{
|
||||
Character& data = *character;
|
||||
progress.weight = value_clamp(progress.weight, data.weight, data.weightMax);
|
||||
progress.capacity = value_clamp(progress.capacity, data.capacityMin, data.capacityMax);
|
||||
progress.digestionRate = value_clamp(progress.digestionRate, data.digestionRateMin, data.digestionRateMax);
|
||||
progress.calories = value_clamp(progress.calories, 0, std::max(0.0f, capacity_max_get()));
|
||||
progress.digestionProgress = value_clamp(progress.digestionProgress, 0, DIGESTION_MAX);
|
||||
progress.digestionTimer = std::clamp(progress.digestionTimer, 0, std::max(0, data.digestionTime));
|
||||
progress.totalCalories = std::max(0.0f, progress.totalCalories);
|
||||
progress.playtime = std::max(0.0f, progress.playtime);
|
||||
}
|
||||
|
||||
// Swaps a layer's region for the one a source layer shows at a time, while active. Turns itself off (and returns
|
||||
// false) when the animation lacks the source or the time runs past it.
|
||||
static bool is_region_swapped(Actor& actor, int source, float& time, Override& swap, bool isActive)
|
||||
{
|
||||
Anm2Animation* animation = actor.animation_get();
|
||||
swap.isRegion = false;
|
||||
if (!isActive || !animation)
|
||||
return false;
|
||||
for (Anm2Item& layer : animation->layers)
|
||||
{
|
||||
if (layer.id != source || layer.frames.empty())
|
||||
continue;
|
||||
Anm2Frame frame = actor.frame_get(layer.frames, time, TARGET_LAYER, source);
|
||||
swap = {.crop = frame.crop, .pivot = frame.pivot, .size = frame.size, .id = swap.id, .isRegion = true};
|
||||
time += TALK_STEP;
|
||||
return true;
|
||||
}
|
||||
time = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
static int source_length_get(Actor& actor, int source)
|
||||
{
|
||||
Anm2Animation* animation = actor.animation_get();
|
||||
if (!animation)
|
||||
return -1;
|
||||
for (Anm2Item& layer : animation->layers)
|
||||
if (layer.id == source)
|
||||
return track_length_get(layer.frames);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void Play::body_update()
|
||||
{
|
||||
Character& data = *character;
|
||||
values_clamp();
|
||||
isJustStageUp = false;
|
||||
isJustStageFinal = false;
|
||||
isJustDigested = false;
|
||||
if (!actor.isPlaying)
|
||||
{
|
||||
if (isStageUp)
|
||||
{
|
||||
(stage >= (int)data.stages.size() ? isJustStageFinal : isJustStageUp) = true;
|
||||
isStageUp = false;
|
||||
actor.isLocked = false;
|
||||
}
|
||||
if (!isTalking)
|
||||
idle_play();
|
||||
}
|
||||
actor.update(*audio);
|
||||
|
||||
if (progress.isDigesting && --progress.digestionTimer <= 0)
|
||||
{
|
||||
float nextWeight = value_clamp(progress.weight + progress.calories * data.caloriesToKilogram, data.weight,
|
||||
data.weightMax);
|
||||
if (is_over_capacity())
|
||||
{
|
||||
float over = progress.calories - progress.capacity;
|
||||
float overPercent = over / (capacity_max_get() - progress.capacity);
|
||||
progress.capacity = value_clamp(progress.capacity + overPercent * data.overstuffedDigestBonus * over,
|
||||
data.capacityMin, data.capacityMax);
|
||||
}
|
||||
progress.calories = 0;
|
||||
if (int nextStage = stage_get(nextWeight); nextStage > stage_get(progress.weight))
|
||||
{
|
||||
actor.interrupt = INTERRUPT_IF_ALLOWED;
|
||||
body_play(data.animationStageUp, TRANSITION_NORMAL, INTERRUPT_NEVER);
|
||||
stage = nextStage;
|
||||
isStageUp = true;
|
||||
actor.isLocked = true;
|
||||
}
|
||||
else
|
||||
isJustDigested = true;
|
||||
progress.weight = nextWeight;
|
||||
progress.isDigesting = false;
|
||||
progress.digestionTimer = data.digestionTime;
|
||||
progress.digestionProgress = 0;
|
||||
}
|
||||
else if (!progress.isDigesting)
|
||||
{
|
||||
if (progress.calories > 0)
|
||||
progress.digestionProgress += progress.digestionRate;
|
||||
if (progress.digestionProgress >= DIGESTION_MAX)
|
||||
{
|
||||
progress.isDigesting = true;
|
||||
progress.digestionTimer = data.digestionTime;
|
||||
audio->play(data.sounds[SOUND_DIGEST]);
|
||||
}
|
||||
}
|
||||
values_clamp();
|
||||
if (is_chance(data.gurgleChance * capacity_percent_get() * data.gurgleCapacityMultiplier * PERCENT))
|
||||
audio->play(data.sounds[SOUND_GURGLE]);
|
||||
stage = stage_get(progress.weight);
|
||||
|
||||
float stageProgress = stage_progress_get();
|
||||
float capacityProgress = capacity_percent_get();
|
||||
if (progress.isDigesting)
|
||||
capacityProgress *= (float)progress.digestionTimer / std::max(1, data.digestionTime);
|
||||
for (int i = 0; i < (int)data.expandAreas.size(); i++)
|
||||
{
|
||||
float scaleMax = data.expandAreas[i].scaleMax;
|
||||
float add = std::clamp((stageProgress + capacityProgress) * scaleMax / 2, 0.0f, scaleMax);
|
||||
actor.overrides[OVERRIDE_EXPAND + i * 2].scale = {add, add};
|
||||
actor.overrides[OVERRIDE_EXPAND + i * 2 + 1].scale = {add, add};
|
||||
}
|
||||
|
||||
isTalking = is_region_swapped(actor, data.talkSource, talkTime, actor.overrides[OVERRIDE_TALK], isTalking);
|
||||
if (talkTime > source_length_get(actor, data.talkSource))
|
||||
talkTime = 0;
|
||||
int blinkLength = source_length_get(actor, data.blinkSource);
|
||||
if (blinkLength != 1)
|
||||
{
|
||||
isBlinking |= is_chance(data.blinkChance);
|
||||
isBlinking = is_region_swapped(actor, data.blinkSource, blinkTime, actor.overrides[OVERRIDE_BLINK], isBlinking);
|
||||
if (blinkTime >= blinkLength)
|
||||
{
|
||||
isBlinking = false;
|
||||
blinkTime = 0;
|
||||
actor.overrides[OVERRIDE_BLINK].isRegion = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Play::camera_update(Vec2 screenSize)
|
||||
// Pans with the middle mouse (or control and the left), zooms through the ladder with the wheel toward the mouse, and
|
||||
// settles the screen shake.
|
||||
void Camera::update(Play& play, Vec2 screenSize)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
Cursor& cursor = play.character->cursor;
|
||||
if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) && !ImGui::IsAnyItemActive())
|
||||
{
|
||||
if (ImGui::IsMouseDown(ImGuiMouseButton_Middle) ||
|
||||
(ImGui::IsMouseDown(ImGuiMouseButton_Left) && ImGui::IsKeyDown(ImGuiMod_Ctrl)))
|
||||
{
|
||||
cursor.play(character->cursorPan);
|
||||
play.cursor.play(cursor.pan);
|
||||
pan = pan - Vec2{io.MouseDelta.x, io.MouseDelta.y} / (zoom / PERCENT);
|
||||
}
|
||||
if (io.MouseWheel != 0)
|
||||
{
|
||||
Vec2 mouse = {io.MousePos.x, io.MousePos.y};
|
||||
Vec2 world = world_get(mouse);
|
||||
cursor.play(character->cursorZoom);
|
||||
play.cursor.play(cursor.zoom);
|
||||
int index = std::clamp(zoom_index_get(zoom) + (io.MouseWheel > 0 ? 1 : -1), 0,
|
||||
(int)std::size(ZOOM_LADDER) - 1);
|
||||
zoom = ZOOM_LADDER[index];
|
||||
@@ -413,7 +350,7 @@ void Play::camera_update(Vec2 screenSize)
|
||||
}
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Home))
|
||||
focus(screenSize);
|
||||
play.focus(screenSize);
|
||||
|
||||
if (shakeTime > 0)
|
||||
{
|
||||
@@ -430,152 +367,3 @@ void Play::camera_update(Vec2 screenSize)
|
||||
shake = {};
|
||||
}
|
||||
}
|
||||
|
||||
// The Konami code unlocks the cheats menu, keyed within a few seconds of each press.
|
||||
void Play::cheats_update()
|
||||
{
|
||||
if (isCheats)
|
||||
return;
|
||||
for (ImGuiKey key : {ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_B,
|
||||
ImGuiKey_A})
|
||||
{
|
||||
if (!ImGui::IsKeyPressed(key, false))
|
||||
continue;
|
||||
cheatIndex = key == CHEAT_CODE[cheatIndex] ? cheatIndex + 1 : key == CHEAT_CODE[0] ? 1 : 0;
|
||||
cheatTime = (float)ImGui::GetTime();
|
||||
if (cheatIndex < (int)std::size(CHEAT_CODE))
|
||||
continue;
|
||||
isCheats = true;
|
||||
cheatIndex = 0;
|
||||
toast_add(STRING_ToastCheatsUnlocked);
|
||||
audio->play(character->sounds[SOUND_CHEATS]);
|
||||
}
|
||||
if (cheatIndex > 0 && ImGui::GetTime() - cheatTime > CHEAT_TIME)
|
||||
cheatIndex = 0;
|
||||
}
|
||||
|
||||
void Play::particles_spawn(const std::vector<std::string>& names, Vec2 position)
|
||||
{
|
||||
for (const std::string& name : names)
|
||||
{
|
||||
auto found = character->particles.find(name);
|
||||
if (found == character->particles.end() || !found->second.anm2)
|
||||
SDL_Log("Particle not found: %s", name.c_str());
|
||||
else
|
||||
particles.emplace_back(found->second.anm2, position, found->second.animation);
|
||||
}
|
||||
}
|
||||
|
||||
void Play::update(Vec2 screenSize)
|
||||
{
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (std::exchange(isFocusPending, false))
|
||||
{
|
||||
focus(screenSize);
|
||||
pan = progress.zoom >= ZOOM_LADDER[0] ? progress.pan : pan;
|
||||
zoom = progress.zoom >= ZOOM_LADDER[0] ? ZOOM_LADDER[zoom_index_get(progress.zoom)] : zoom;
|
||||
}
|
||||
progress.playtime += io.DeltaTime;
|
||||
cheats_update();
|
||||
text.update(*this, screenSize);
|
||||
|
||||
if (isJustStageFinal && !isPostgame && !isPostgamePending)
|
||||
{
|
||||
Entry* end = character->dialogue.hook_get("End");
|
||||
isPostgame = isCheats = !end;
|
||||
if (end)
|
||||
{
|
||||
text.set(*this, end, TEXT_UNINTERRUPTIBLE);
|
||||
isMenuOpen = isToolsOpen = false;
|
||||
progress.calories = 0;
|
||||
progress.digestionProgress = 0;
|
||||
item_drop();
|
||||
isPostgamePending = true;
|
||||
focus(screenSize);
|
||||
}
|
||||
}
|
||||
if (isPostgamePending && (!text.entry || (text.entry->isTerminal && text.isFinished)) && !text.isScene)
|
||||
{
|
||||
isMenuOpen = isPostgame = isCheats = true;
|
||||
isPostgamePending = false;
|
||||
focus(screenSize);
|
||||
}
|
||||
|
||||
if (isAutofeed)
|
||||
for (int i = 0; i < (int)character->interactTypes.size(); i++)
|
||||
if (character->interactTypes[i].name == AUTOFEED_TYPE)
|
||||
cursorType = i;
|
||||
Pointer pointer;
|
||||
pointer.position = world_get({io.MousePos.x, io.MousePos.y});
|
||||
pointer.type = cursorType;
|
||||
pointer.isCaptured = io.WantCaptureMouse;
|
||||
pointer.isClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
|
||||
pointer.isDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
|
||||
pointer.isReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
||||
pointer.isRightClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
|
||||
Pointer itemPointer = pointer;
|
||||
Pointer interactPointer = pointer;
|
||||
if (text.isScene)
|
||||
{
|
||||
itemPointer = interactPointer = {.position = pointer.position, .isCaptured = true};
|
||||
isAutofeedActive = false;
|
||||
}
|
||||
else
|
||||
autofeed_update(itemPointer, interactPointer);
|
||||
items_update(itemPointer);
|
||||
interact_update(interactPointer);
|
||||
|
||||
if (character->cursor && !isAutofeedActive && pointer.isClicked && !isItemHovered && !isHovering &&
|
||||
cursorState == CURSOR_DEFAULT)
|
||||
cursor.play(character->cursorClick, TRANSITION_FORCE, INTERRUPT_ALWAYS);
|
||||
|
||||
body_update();
|
||||
int areaIndex = character->area_get(stage);
|
||||
if (areaIndex >= 0 && area.anm2 != character->areas[areaIndex].anm2)
|
||||
area = Actor(character->areas[areaIndex].anm2);
|
||||
if (areaIndex >= 0 && is_rect_valid(character->areas[areaIndex].bounds))
|
||||
bounds = character->areas[areaIndex].bounds;
|
||||
area.update(*audio);
|
||||
|
||||
for (int i = 0; i < (int)particles.size(); i++)
|
||||
{
|
||||
particles[i].update(*audio);
|
||||
if (!particles[i].isPlaying)
|
||||
particles.erase(particles.begin() + i--);
|
||||
}
|
||||
for (Actor* each : {&cursor, &autoCursor})
|
||||
{
|
||||
bool isActive = (each == &autoCursor) == isAutofeedActive;
|
||||
if ((!isActive || cursorState == CURSOR_DEFAULT) && !each->pending)
|
||||
each->play(character->cursorIdle, TRANSITION_IF_IDLE);
|
||||
each->update(*audio);
|
||||
}
|
||||
cursorState = CURSOR_DEFAULT;
|
||||
cursor.position = {io.MousePos.x, io.MousePos.y};
|
||||
cursor.isVisible = true;
|
||||
camera_update(screenSize);
|
||||
|
||||
for (int i = 0; i < (int)toasts.size(); i++)
|
||||
if (--toasts[i].life <= 0)
|
||||
toasts.erase(toasts.begin() + i--);
|
||||
if ((autosaveTime += io.DeltaTime) > AUTOSAVE_TIME)
|
||||
{
|
||||
save();
|
||||
autosaveTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Play::render(Renderer& renderer, Vec2 screenSize)
|
||||
{
|
||||
float scale = zoom / PERCENT;
|
||||
Affine view = {scale, 0, 0, scale, -pan.x * scale + shake.x * screenSize.x, -pan.y * scale + shake.y * screenSize.y};
|
||||
area.render(renderer, view);
|
||||
actor.render(renderer, view);
|
||||
if (!text.isScene)
|
||||
for (ItemEntity& entity : items)
|
||||
entity.actor.render(renderer, view);
|
||||
if (isAutofeedActive)
|
||||
autoCursor.render(renderer, view);
|
||||
for (Actor& particle : particles)
|
||||
particle.render(renderer, view);
|
||||
}
|
||||
Reference in new issue
Block a user