#include "play.hpp" #include "play/style.hpp" #include #include #include #include #include "../util/imgui.hpp" #include "../util/imgui/style.hpp" #include "../util/imgui/widget.hpp" #include "../util/math.hpp" using namespace game::resource; using namespace game::resource::xml; using namespace game::util; using namespace game::state::play; using namespace glm; namespace game::state { void entity_debug_flags_apply(Entity& entity, bool isShowLayers, bool isShowNulls) { entity.isShowLayers = isShowLayers; entity.isShowNulls = isShowNulls; } int saved_item_id_get(const Schema& itemSchema, const Schema::Element& item) { if (item.id >= 0 && item.id < (int)itemSchema.items.size() && item.id < (int)itemSchema.anm2s.size()) return item.id; return -1; } World::Focus Play::focus_get() { if (text.is_scene_active()) return World::CENTER; auto isToolsOpen = tools.isOpen; return menu.isOpen && isToolsOpen ? World::MENU_TOOLS : menu.isOpen ? World::MENU : isToolsOpen ? World::TOOLS : World::CENTER; } bool Play::cursor_enabled_get() const { auto* root = character.data.cursorSchema.root(); return root && root->type == Schema::Element::CURSOR; } void Play::debug_flags_apply() { entity_debug_flags_apply(character, isShowLayers, isShowNulls); entity_debug_flags_apply(cursor, isShowLayers, isShowNulls); entity_debug_flags_apply(autofeedCursor, isShowLayers, isShowNulls); areaManager.debug_flags_set(isShowLayers, isShowNulls); for (auto& item : itemManager.items) entity_debug_flags_apply(item, isShowLayers, isShowNulls); for (auto& particle : particleManager.particles) entity_debug_flags_apply(particle, isShowLayers, isShowNulls); } void Play::set(Resources& resources, int selectedCharacterIndex, enum Game game) { auto& data = resources.character_get(selectedCharacterIndex); auto* saveRoot = data.save.root(); auto isSaveValid = saveRoot && saveRoot->type == Schema::Element::SAVE; auto* saveCharacter = isSaveValid ? data.save.child_get(*saveRoot, Schema::Element::CHARACTER) : nullptr; auto* saveItems = isSaveValid ? data.save.child_get(*saveRoot, Schema::Element::ITEMS) : nullptr; auto* saveDiscovered = isSaveValid ? data.save.child_get(*saveRoot, Schema::Element::DISCOVERED) : nullptr; auto* savePan = isSaveValid ? data.save.child_get(*saveRoot, Schema::Element::PAN) : nullptr; auto* saveZoom = isSaveValid ? data.save.child_get(*saveRoot, Schema::Element::ZOOM) : nullptr; auto& itemSchema = data.itemSchema; auto& dialogue = data.dialogue; auto& menuSchema = data.menuSchema; auto* characterRoot = data.root(); this->characterIndex = selectedCharacterIndex; resources.last_character_save(selectedCharacterIndex); cheatCodeIndex = 0; cheatCodeStartTime = 0.0; world = play::World{}; character = Entity(data, vec2{}); character.digestionRate = glm::clamp(character.digestionRate, characterRoot->digestionRateMin, characterRoot->digestionRateMax); character.capacity = glm::clamp(character.capacity, characterRoot->capacityMinCalories, characterRoot->capacityMaxCalories); auto* alternateSpritesheet = data.alternate_spritesheet(); auto isAlternateSpritesheet = alternateSpritesheet && game == NEW_GAME && math::random_percent_roll(alternateSpritesheet->chanceOnNewGame); if (isAlternateSpritesheet || (isSaveValid && saveRoot->isAlternateSpritesheet)) { character.spritesheet_set(Entity::ALTERNATE); if (game == NEW_GAME && alternateSpritesheet) alternateSpritesheet->soundEntry.sound.play(); } character.totalCaloriesConsumed = saveCharacter ? saveCharacter->totalCaloriesConsumed : 0; character.totalItemsSpawned = saveCharacter ? saveCharacter->totalItemsSpawned : 0; character.totalItemsConsumed = saveCharacter ? saveCharacter->totalItemsConsumed : 0; character.totalPlaytimeSeconds = saveCharacter ? saveCharacter->totalPlaytimeSeconds : 0; autofeed = play::Autofeed{}; characterManager = CharacterManager{}; particleManager = ParticleManager{}; areaManager = AreaManager{}; areaManager.set(character); world.bounds_set(areaManager.bounds_get(character, World::BOUNDS)); character.position = {}; cursor = Entity{}; cursor.entityType = CURSOR; cursor.interactTypeID = character.data.interact_type_names_get().empty() ? -1 : 0; autofeedCursor = Entity{}; autofeedCursor.entityType = AUTOCURSOR; autofeedCursor.interactTypeID = cursor.interactTypeID; autofeedCursor.position = cursor.position; autofeedCursor.isVisible = false; if (cursor_enabled_get()) { cursor = Entity(character.data.cursorSchema.baseAnm2); cursor.entityType = CURSOR; cursor.interactTypeID = character.data.interact_type_names_get().empty() ? -1 : 0; autofeedCursor = Entity(character.data.cursorSchema.baseAnm2); autofeedCursor.entityType = AUTOCURSOR; autofeedCursor.interactTypeID = cursor.interactTypeID; autofeedCursor.position = cursor.position; autofeedCursor.isVisible = false; } menu.inventory = play::menu::Inventory{}; itemManager = ItemManager{}; itemManager.spawnDelayTicks = saveRoot ? saveRoot->spawnDelayTicks : 0; itemManager.discoveredItemIDs.resize(itemSchema.items.size()); for (auto* item : saveDiscovered ? data.save.children_get(*saveDiscovered, Schema::Element::ITEM) : std::vector{}) { auto itemID = saved_item_id_get(itemSchema, *item); if (itemID != -1) itemManager.discoveredItemIDs[itemID] = true; } for (auto* item : saveItems ? data.save.children_get(*saveItems, Schema::Element::ITEM) : std::vector{}) { auto itemID = saved_item_id_get(itemSchema, *item); if (itemID == -1) continue; auto& saveItem = itemSchema.anm2s[itemID]; itemManager.items.emplace_back(saveItem, item->position, itemID, item->velocity, item->rotation); itemManager.discoveredItemIDs[itemID] = true; } imgui::style::widget_set(menuSchema.rounding); imgui::widget::sounds_set(&menuSchema.root()->soundHover, &menuSchema.root()->soundSelect); play::style::color_set(resources, character); text.entry = nullptr; text.isEnabled = false; isPostgamePending = false; #if DEBUG menu.isCheats = true; #else menu.isCheats = false; #endif isPostgame = isSaveValid && saveRoot->isPostgame; if (character.stage_get() >= character.stage_max_get()) isPostgame = true; if (isPostgame) menu.isCheats = true; if (auto font = character.data.menuSchema.font.get()) ImGui::GetIO().FontDefault = font; character.play({.animation = character.idle_animation_get(), .appendID = character.animation_append_id_get()}); character.update(); worldCanvas.size_set(imgui::to_vec2(ImGui::GetMainViewport()->Size)); world.set(character, worldCanvas, focus_get()); if (savePan) worldCanvas.pan = savePan->position; if (saveZoom && saveZoom->zoom >= World::ZOOM_MIN) { worldCanvas.zoom = glm::clamp(World::ZOOM_MIN, saveZoom->zoom, World::ZOOM_MAX); world.zoom_index_set(worldCanvas); } if (game == NEW_GAME) text.set(dialogue.dialogue_hook_entry_get(Schema::Element::START), character, false); if (isPostgame) { isPostgamePending = false; } else { isPostgamePending = false; } } void Play::exit(Resources& resources) { imgui::style::color_set(resources.settings.root()->color); imgui::style::widget_set(); imgui::widget::sounds_set(nullptr, nullptr); ImGui::GetIO().FontDefault = resources.font.get(); save(resources); } void Play::update(Resources& resources) { static constexpr std::array CHEAT_CODE = { ImGuiKey_UpArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_DownArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_B, ImGuiKey_A}; static constexpr std::array CHEAT_INPUT_KEYS = { ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_B, ImGuiKey_A}; static constexpr auto CHEAT_CODE_INPUT_TIME_SECONDS = 5.0; static constexpr auto UI_ALPHA_MAX = 1.0f; auto focus = focus_get(); auto& dialogue = character.data.dialogue; auto isCursorEnabled = cursor_enabled_get(); character.totalPlaytimeSeconds += ImGui::GetIO().DeltaTime; if (isCursorEnabled) { cursor.isVisible = true; autofeedCursor.isVisible = false; } if (!menu.isCheats) { for (auto key : CHEAT_INPUT_KEYS) { if (!ImGui::IsKeyPressed(key, false)) continue; if (key == CHEAT_CODE[cheatCodeIndex]) { cheatCodeIndex++; cheatCodeStartTime = ImGui::GetTime(); } else if (key == CHEAT_CODE[0]) { cheatCodeIndex = 1; cheatCodeStartTime = ImGui::GetTime(); } else { cheatCodeIndex = 0; cheatCodeStartTime = 0.0; } if (cheatCodeIndex >= (int)CHEAT_CODE.size()) { menu.isCheats = true; cheatCodeIndex = 0; cheatCodeStartTime = 0.0; toasts.push(character.data.strings.get(Strings::ToastCheatsUnlocked)); character.data.menuSchema.root()->soundCheatsActivated.play(); } } if (cheatCodeIndex > 0 && (ImGui::GetTime() - cheatCodeStartTime > CHEAT_CODE_INPUT_TIME_SECONDS)) { cheatCodeIndex = 0; cheatCodeStartTime = 0.0; } } auto isSceneActive = text.is_scene_active(); if (!isSceneActive) { tools.update(character, cursor, world, focus, worldCanvas, false, UI_ALPHA_MAX); menu.update(resources, itemManager, character, text, autofeed); toasts.update(); } itemManager.isDisabled = isSceneActive; characterManager.isDisabled = isSceneActive; if (text.isEnabled) text.update(character, worldCanvas); if (character.isJustStageFinal && !isPostgame && !isPostgamePending) { auto* end = dialogue.dialogue_hook_entry_get(Schema::Element::END); if (end) { text.set(end, character, false); tools.isOpen = false; menu.isOpen = false; character.calories = 0; character.digestionProgress = 0; itemManager.heldItemIndex = -1; isPostgamePending = true; world.character_focus(character, worldCanvas, focus_get()); } else { isPostgame = true; menu.isCheats = true; } } if (isPostgamePending && text.is_terminal() && text.is_finished() && !text.is_scene_active()) { menu.isOpen = true; isPostgamePending = false; isPostgame = true; menu.isCheats = true; world.character_focus(character, worldCanvas, focus_get()); } if (!text.is_scene_active()) autofeed.update(resources, character, autofeedCursor, menu.inventory, itemManager, characterManager, worldCanvas); auto& interactionCursor = autofeed.active_get() ? autofeedCursor : cursor; itemManager.isAutomation = autofeed.active_get(); itemManager.update(character, interactionCursor, areaManager, text, world.bounds, worldCanvas); auto* itemRoot = character.data.itemSchema.root(); for (int i = 0; i < itemManager.newItemFoundCount; i++) { toasts.push(character.data.strings.get(Strings::ToastNewItemFound)); if (itemRoot) itemRoot->soundNewItem.play(); } characterManager.update(character, interactionCursor, text, worldCanvas); for (auto& particle : itemManager.particles) particleManager.spawn(character.data, particle.label, particle.position); for (auto& particle : characterManager.particles) particleManager.spawn(character.data, particle.label, particle.position); if (isCursorEnabled && !autofeed.active_get() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !itemManager.isItemHovered && !characterManager.isHovering && cursor.cursorState == Entity::DEFAULT) if (auto* cursorRoot = character.data.cursorSchema.root()) if (auto animation = cursorRoot->animationClick.get()) cursor.play({.animation = *animation, .mode = Entity::PLAY_FORCE, .interrupt = Entity::Interrupt::ALWAYS}); character.update(); areaManager.update(character); world.bounds_set(areaManager.bounds_get(character, world.bounds)); particleManager.update(); cursor.update(); if (autofeed.active_get()) autofeedCursor.update(); world.update(character, cursor, worldCanvas, focus); worldCanvas.update(); if (autosaveTime += ImGui::GetIO().DeltaTime; autosaveTime > AUTOSAVE_TIME || menu.settingsMenu.isSave) { save(resources); autosaveTime = 0; menu.settingsMenu.isSave = false; } } void Play::render(Resources& resources, Canvas& canvas) { auto& textureShader = resources.shaders[shader::TEXTURE]; auto& rectShader = resources.shaders[shader::RECT]; auto size = imgui::to_ivec2(ImGui::GetMainViewport()->Size); auto windowModel = math::quad_model_get(vec2(size)); worldCanvas.bind(); worldCanvas.size_set(size); worldCanvas.clear(); debug_flags_apply(); areaManager.render(character, textureShader, rectShader, worldCanvas); character.render(textureShader, rectShader, worldCanvas); if (!text.is_scene_active()) for (auto& item : itemManager.items) item.render(textureShader, rectShader, worldCanvas); auto isCursorEnabled = cursor_enabled_get(); if (isCursorEnabled && autofeed.active_get()) autofeedCursor.render(textureShader, rectShader, worldCanvas); particleManager.render(textureShader, rectShader, worldCanvas); worldCanvas.unbind(); auto isCursor = isCursorEnabled && !resources.isDeveloper; canvas.bind(); canvas.texture_render(textureShader, worldCanvas, windowModel); ImGui::Render(); ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); if (isCursor) cursor.render(textureShader, rectShader, canvas); canvas.unbind(); } void Play::save(Resources& resources) { resource::xml::Schema save; auto& itemSchema = character.data.itemSchema; auto& root = save.element_add(resource::xml::Schema::Element::SAVE); root.isPostgame = isPostgame; root.isAlternateSpritesheet = character.spritesheetType == Entity::ALTERNATE; root.spawnDelayTicks = itemManager.spawnDelayTicks; auto& panElement = save.element_add(resource::xml::Schema::Element::PAN, 0); panElement.position = worldCanvas.pan; auto& zoomElement = save.element_add(resource::xml::Schema::Element::ZOOM, 0); zoomElement.zoom = worldCanvas.zoom; auto& characterElement = save.element_add(resource::xml::Schema::Element::CHARACTER, 0); characterElement.weightKilograms = character.weight; characterElement.calories = character.calories; characterElement.capacityCalories = character.capacity; characterElement.digestionRate = character.digestionRate; characterElement.digestionProgress = character.digestionProgress; characterElement.isDigesting = character.isDigesting; characterElement.digestionTimer = character.digestionTimer; characterElement.totalCaloriesConsumed = character.totalCaloriesConsumed; characterElement.totalItemsSpawned = character.totalItemsSpawned; characterElement.totalItemsConsumed = character.totalItemsConsumed; characterElement.totalPlaytimeSeconds = character.totalPlaytimeSeconds; auto itemsIndex = (int)save.elements.size(); save.element_add(resource::xml::Schema::Element::ITEMS, 0); for (auto& item : itemManager.items) { if (item.schemaID < 0 || item.schemaID >= (int)itemSchema.items.size()) continue; auto& itemElement = save.element_add(resource::xml::Schema::Element::ITEM, itemsIndex); itemElement.id = item.schemaID; itemElement.position = item.position; itemElement.velocity = item.velocity; itemElement.rotation = *item.overrides[item.rotationOverrideID].frame.rotation; } auto discoveredIndex = (int)save.elements.size(); save.element_add(resource::xml::Schema::Element::DISCOVERED, 0); for (int i = 0; i < (int)itemManager.discoveredItemIDs.size(); i++) { if (!itemManager.discoveredItemIDs[i]) continue; auto& itemElement = save.element_add(resource::xml::Schema::Element::ITEM, discoveredIndex); itemElement.id = i; } resources.character_save_set(characterIndex, save); save.serialize(character.data.save_path_get()); toasts.push(character.data.strings.get(Strings::ToastSaving)); } };