Files
shweets-sim/src/play.cpp
T

370 lines
13 KiB
C++

#include "play.h"
#include "renderer.h"
#include <imgui.h>
#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 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;
constexpr float FOCUS_PADDING = 100;
constexpr float FOCUS_TOOLS_WIDTH = 0.1f;
constexpr float SHAKE_DECAY = 0.97f;
constexpr float SHAKE_EPSILON = 0.1f;
constexpr float SHAKE_LERP = 0.72f;
constexpr float SHAKE_MAGNITUDE = 0.1f;
struct SavedItem
{
std::string name;
Vec2 position;
Vec2 velocity;
float rotation = 0;
};
static int zoom_index_get(float zoom)
{
int index = 0;
for (int i = 1; i < (int)std::size(ZOOM_LADDER); i++)
if (std::abs(zoom - ZOOM_LADDER[i]) < std::abs(zoom - ZOOM_LADDER[index]))
index = i;
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)
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();
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)
{
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());
}
cursorType = played.interactTypes.empty() ? -1 : 0;
if (played.cursor.anm2)
{
cursor = Actor(played.cursor.anm2);
autoCursor = Actor(played.cursor.anm2);
autoCursor.isVisible = false;
}
isPostgame |= body.stage >= (int)played.stages.size();
#ifdef DEBUG
isCheats = true;
#endif
isCheats |= isPostgame;
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(HOOK_START), TEXT_UNINTERRUPTIBLE);
}
void Play::save()
{
if (!character)
return;
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].id + "\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].id +
"\n";
file_write(character->save_path_get(), text);
toast_add("Saving...");
}
// Shows the area of the body's stage, with its bounds.
void Play::area_set()
{
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::toast_add(const std::string& message)
{
toasts.push_back({message, TOAST_LIFE});
}
Actor& Play::pointer_actor_get()
{
return autofeed.isActive ? autoCursor : cursor;
}
Focus Play::focus_get()
{
if (text.isScene)
return FOCUS_CENTER;
return isMenuOpen && isToolsOpen ? FOCUS_BOTH : isMenuOpen ? FOCUS_MENU : isToolsOpen ? FOCUS_TOOLS : FOCUS_CENTER;
}
void Play::focus(Vec2 screenSize)
{
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;
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(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;
}
// 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)))
{
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);
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];
pan = world - mouse / (zoom / PERCENT);
}
}
if (ImGui::IsKeyPressed(ImGuiKey_Home))
play.focus(screenSize);
if (shakeTime > 0)
{
shakeTime--;
float factor = (float)shakeTime / std::max(1, shakeTimeMax);
float magnitude = std::max(vec2_length(shake) * SHAKE_DECAY, SHAKE_MAGNITUDE * factor);
Vec2 target = Vec2{random_get(-magnitude, magnitude), random_get(-magnitude, magnitude)} * factor;
shake = vec2_lerp(shake, target, SHAKE_LERP);
}
else
{
shake = vec2_lerp(shake, {}, SHAKE_LERP);
if (vec2_length(shake) < SHAKE_EPSILON)
shake = {};
}
}