lfs, current build
This commit is contained in:
1 parent
2efed2a443
commit
a5904b917b
397 files changed
+215918
-2458
No files matched your search
+91
-17
@@ -4,6 +4,7 @@
|
||||
#include <charconv>
|
||||
#include <format>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
#include <imgui.h>
|
||||
@@ -24,14 +25,16 @@ namespace game::developer
|
||||
X(DIGEST, "digest", "digest", true) \
|
||||
X(DIGESTION, "digestion", "digestion <% per second>", true) \
|
||||
X(DIALOGUE, "dialogue", "dialogue", true) \
|
||||
X(EAT_SPEED, "eat_speed", "eat_speed <multiplier>", true) \
|
||||
X(GIVE, "give", "give <item_id> <quantity>", true) \
|
||||
X(HELP, "help", "help", false) \
|
||||
X(ITEMS, "items", "items", true) \
|
||||
X(MENU, "menu", "menu", true) \
|
||||
X(NEW_GAME, "new_game", "new_game <archive>", false) \
|
||||
X(PLAY_ANIMATION, "play_animation", "play_animation <animation>", true) \
|
||||
X(PLAY_DIALOGUE, "play_dialogue", "play_dialogue <label>", true) \
|
||||
X(RELOAD, "reload", "reload", false) \
|
||||
X(SHOW_LAYERS, "show_layers", "show_layers [true/false]", true) \
|
||||
X(SHOW_NULLS, "show_nulls", "show_nulls [true/false]", true) \
|
||||
X(SPAWN, "spawn", "spawn <item_id>", true) \
|
||||
X(STAGE, "stage", "stage <stage>", true) \
|
||||
X(WEIGHT, "weight", "weight <kilograms>", true)
|
||||
|
||||
@@ -65,7 +68,6 @@ namespace game::developer
|
||||
constexpr auto SCROLL_EPSILON = 1.0f;
|
||||
constexpr auto INPUT_FLAGS = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackHistory;
|
||||
constexpr auto ARG_VALUE = 1;
|
||||
constexpr auto ARG_QUANTITY = ARG_VALUE + 1;
|
||||
constexpr auto HISTORY_PREVIOUS = -1;
|
||||
constexpr auto HISTORY_NEXT = 1;
|
||||
|
||||
@@ -92,6 +94,19 @@ namespace game::developer
|
||||
return result;
|
||||
}
|
||||
|
||||
bool toggle_value_get(const std::vector<std::string_view>& args, bool isCurrent, bool& isValid)
|
||||
{
|
||||
isValid = true;
|
||||
if (args.size() <= ARG_VALUE) return !isCurrent;
|
||||
|
||||
auto value = args[ARG_VALUE];
|
||||
if (value == "true") return true;
|
||||
if (value == "false") return false;
|
||||
|
||||
isValid = false;
|
||||
return isCurrent;
|
||||
}
|
||||
|
||||
std::vector<std::string_view> args_get(const std::string& input)
|
||||
{
|
||||
std::vector<std::string_view> result{};
|
||||
@@ -229,20 +244,6 @@ namespace game::developer
|
||||
history_push(std::format("Playing dialogue: {}", label));
|
||||
break;
|
||||
}
|
||||
case Command::EAT_SPEED:
|
||||
{
|
||||
play->character.eatSpeed = float_parse(args.size() > ARG_VALUE ? args[ARG_VALUE] : std::string_view{});
|
||||
history_push(std::format("Eat speed set to {}x.", play->character.eatSpeed));
|
||||
break;
|
||||
}
|
||||
case Command::GIVE:
|
||||
{
|
||||
auto itemID = int_parse(args.size() > ARG_VALUE ? args[ARG_VALUE] : std::string_view{});
|
||||
auto quantity = int_parse(args.size() > ARG_QUANTITY ? args[ARG_QUANTITY] : std::string_view{});
|
||||
play->menu.inventory.values[itemID] += quantity;
|
||||
history_push(std::format("Gave item {} x{}.", itemID, quantity));
|
||||
break;
|
||||
}
|
||||
case Command::HELP:
|
||||
help_print(isGame);
|
||||
break;
|
||||
@@ -260,6 +261,27 @@ namespace game::developer
|
||||
history_push("Returning to menu.");
|
||||
break;
|
||||
}
|
||||
case Command::NEW_GAME:
|
||||
{
|
||||
auto key = args.size() > ARG_VALUE ? std::string{args[ARG_VALUE]} : std::string{};
|
||||
if (key.empty())
|
||||
{
|
||||
history_push(std::format("Usage: {}", info->usage));
|
||||
break;
|
||||
}
|
||||
|
||||
auto characterIndex = resources.character_index_get(key);
|
||||
if (characterIndex < 0)
|
||||
{
|
||||
history_push(std::format("Unknown character: {}", key));
|
||||
break;
|
||||
}
|
||||
|
||||
action.character = characterIndex;
|
||||
action.isNewGame = true;
|
||||
history_push(std::format("Starting new game: {}.", key));
|
||||
break;
|
||||
}
|
||||
case Command::PLAY_ANIMATION:
|
||||
{
|
||||
auto animation = args.size() > ARG_VALUE ? std::string{args[ARG_VALUE]} : std::string{};
|
||||
@@ -283,6 +305,58 @@ namespace game::developer
|
||||
history_push(std::format("Reloaded character archives: {}", resources.characters.size()));
|
||||
break;
|
||||
}
|
||||
case Command::SHOW_LAYERS:
|
||||
{
|
||||
auto isValid = true;
|
||||
auto isShowLayers = toggle_value_get(args, play->isShowLayers, isValid);
|
||||
if (!isValid)
|
||||
{
|
||||
history_push(std::format("Usage: {}", info->usage));
|
||||
break;
|
||||
}
|
||||
|
||||
play->isShowLayers = isShowLayers;
|
||||
play->debug_flags_apply();
|
||||
history_push(std::format("Show layers {}.", isShowLayers ? "enabled" : "disabled"));
|
||||
break;
|
||||
}
|
||||
case Command::SHOW_NULLS:
|
||||
{
|
||||
auto isValid = true;
|
||||
auto isShowNulls = toggle_value_get(args, play->isShowNulls, isValid);
|
||||
if (!isValid)
|
||||
{
|
||||
history_push(std::format("Usage: {}", info->usage));
|
||||
break;
|
||||
}
|
||||
|
||||
play->isShowNulls = isShowNulls;
|
||||
play->debug_flags_apply();
|
||||
history_push(std::format("Show nulls {}.", isShowNulls ? "enabled" : "disabled"));
|
||||
break;
|
||||
}
|
||||
case Command::SPAWN:
|
||||
{
|
||||
auto value = args.size() > ARG_VALUE ? args[ARG_VALUE] : std::string_view{};
|
||||
int itemID{};
|
||||
auto parsed = std::from_chars(value.data(), value.data() + value.size(), itemID);
|
||||
auto isItemIDValid = !value.empty() && parsed.ec == std::errc{} && parsed.ptr == value.data() + value.size();
|
||||
if (!isItemIDValid)
|
||||
{
|
||||
history_push(std::format("Usage: {}", info->usage));
|
||||
break;
|
||||
}
|
||||
|
||||
if (!play->itemManager.spawn_queue(play->character, itemID, true))
|
||||
{
|
||||
history_push(std::format("Unable to spawn item {}.", itemID));
|
||||
break;
|
||||
}
|
||||
|
||||
auto& item = play->character.data.itemSchema.items[itemID];
|
||||
history_push(std::format("Spawned item {} {}.", itemID, item.name));
|
||||
break;
|
||||
}
|
||||
case Command::STAGE:
|
||||
{
|
||||
auto stage = int_parse(args.size() > ARG_VALUE ? args[ARG_VALUE] : std::string_view{});
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace game::developer
|
||||
{
|
||||
int character{-1};
|
||||
bool isMenu{};
|
||||
bool isNewGame{};
|
||||
};
|
||||
|
||||
Action action{};
|
||||
|
||||
+296
-66
@@ -37,6 +37,7 @@ namespace game::developer
|
||||
X(OSCILLATION, "Oscillation", true) \
|
||||
X(SCREEN_SHAKE, "Screenshake", false) \
|
||||
X(SHAKE, "Shake", true) \
|
||||
X(SILENT, "Silent", true) \
|
||||
X(SMALL, "Small", true) \
|
||||
X(SOUND, "Sound", false)
|
||||
|
||||
@@ -154,6 +155,7 @@ namespace game::developer
|
||||
case DialogueEffectType::OSCILLATION:
|
||||
break;
|
||||
case DialogueEffectType::SHAKE:
|
||||
case DialogueEffectType::SILENT:
|
||||
break;
|
||||
case DialogueEffectType::SCREEN_SHAKE:
|
||||
break;
|
||||
@@ -180,7 +182,6 @@ namespace game::developer
|
||||
std::string fontPath{};
|
||||
glm::vec3 color{1.0f, 1.0f, 1.0f};
|
||||
bool isHasColor{};
|
||||
const resource::Audio* blipAudio{};
|
||||
};
|
||||
|
||||
std::filesystem::path resource_absolute_path_get(const std::filesystem::path& schemaPath,
|
||||
@@ -311,7 +312,6 @@ namespace game::developer
|
||||
info.blip = element.path;
|
||||
else if (element.soundEntry.is_valid())
|
||||
info.blip = element.soundEntry.path;
|
||||
if (element.soundEntry.is_valid()) info.blipAudio = &element.soundEntry.sound;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,11 +342,7 @@ namespace game::developer
|
||||
}
|
||||
if (info.blip.empty())
|
||||
{
|
||||
if (auto* blip = root->soundBlip.get())
|
||||
{
|
||||
info.blip = "<cached blip>";
|
||||
info.blipAudio = blip;
|
||||
}
|
||||
if (root->soundBlip.get()) info.blip = "<cached blip>";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,6 +353,16 @@ namespace game::developer
|
||||
return info;
|
||||
}
|
||||
|
||||
const resource::Audio* preview_blip_get(const Schema& schema, const ReferenceInfo* reference)
|
||||
{
|
||||
auto* root = schema.root();
|
||||
auto* blip = root ? root->soundBlip.get() : nullptr;
|
||||
if (blip) return blip;
|
||||
|
||||
root = reference && reference->schema ? reference->schema->root() : nullptr;
|
||||
return root ? root->soundBlip.get() : nullptr;
|
||||
}
|
||||
|
||||
int container_index_get(const Schema& schema, Element::Type type)
|
||||
{
|
||||
auto it = schema.elementsByType.find(type);
|
||||
@@ -416,14 +422,21 @@ namespace game::developer
|
||||
|
||||
void element_selection_range_set(Schema& schema, int& selected, std::vector<int>& selectedElements, int index)
|
||||
{
|
||||
if (!editor_schema::element_index_is_valid(schema, index))
|
||||
{
|
||||
element_selection_set(selected, selectedElements, -1);
|
||||
return;
|
||||
}
|
||||
|
||||
auto parent = schema.elements[index].parent;
|
||||
if (selected < 0 || selected >= (int)schema.elements.size() ||
|
||||
schema.elements[selected].parent != schema.elements[index].parent)
|
||||
schema.elements[selected].parent != parent || !editor_schema::element_index_is_valid(schema, parent))
|
||||
{
|
||||
element_selection_set(selected, selectedElements, index);
|
||||
return;
|
||||
}
|
||||
|
||||
auto& siblings = schema.elements[schema.elements[index].parent].children;
|
||||
auto& siblings = schema.elements[parent].children;
|
||||
auto begin = std::ranges::find(siblings, selected);
|
||||
auto end = std::ranges::find(siblings, index);
|
||||
if (begin == siblings.end() || end == siblings.end())
|
||||
@@ -521,10 +534,12 @@ namespace game::developer
|
||||
return true;
|
||||
}
|
||||
|
||||
int elements_paste(util::Clipboard& clipboard, Schema& schema, int parent, int beforeSibling)
|
||||
int elements_paste(util::Clipboard& clipboard, Schema& schema, int parent, int beforeSibling,
|
||||
Element::Type expected = Element::UNKNOWN)
|
||||
{
|
||||
auto* text = clipboard.get();
|
||||
if (!text || !*text) return -1;
|
||||
if (!editor_schema::element_index_is_valid(schema, parent)) return -1;
|
||||
|
||||
tinyxml2::XMLDocument document{};
|
||||
if (document.Parse(text) == tinyxml2::XML_SUCCESS && document.RootElement() &&
|
||||
@@ -534,12 +549,17 @@ namespace game::developer
|
||||
for (auto* child = document.RootElement()->FirstChildElement(); child; child = child->NextSiblingElement())
|
||||
{
|
||||
auto inserted = schema.element_insert_xml(xml_element_text_get(child), parent, beforeSibling);
|
||||
if (inserted >= 0 && expected != Element::UNKNOWN && schema.elements[inserted].type != expected)
|
||||
{
|
||||
schema.element_erase(inserted);
|
||||
continue;
|
||||
}
|
||||
if (inserted >= 0) selected = inserted;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
return editor_schema::element_paste(clipboard, schema, parent, beforeSibling);
|
||||
return editor_schema::element_paste(clipboard, schema, parent, beforeSibling, expected);
|
||||
}
|
||||
|
||||
bool selected_elements_duplicate(Schema& schema, int& selected, std::vector<int>& selectedElements,
|
||||
@@ -566,10 +586,12 @@ namespace game::developer
|
||||
bool elements_move(Schema& schema, const std::vector<int>& selectedElements, int parent, int beforeSibling)
|
||||
{
|
||||
if (selectedElements.empty()) return false;
|
||||
if (!editor_schema::element_index_is_valid(schema, parent)) return false;
|
||||
auto isDirty = false;
|
||||
for (auto index : selectedElements)
|
||||
{
|
||||
if (index == beforeSibling) continue;
|
||||
if (!editor_schema::element_index_is_valid(schema, index)) continue;
|
||||
if (schema.element_move(index, parent, beforeSibling)) isDirty = true;
|
||||
}
|
||||
return isDirty;
|
||||
@@ -588,6 +610,7 @@ namespace game::developer
|
||||
const std::vector<int>* selectedElements = nullptr)
|
||||
{
|
||||
auto isDirty = false;
|
||||
if (!editor_schema::element_index_is_valid(schema, index)) return isDirty;
|
||||
if (ImGui::BeginDragDropSource())
|
||||
{
|
||||
ImGui::SetDragDropPayload(payloadName, &index, sizeof(index));
|
||||
@@ -783,6 +806,18 @@ namespace game::developer
|
||||
return result;
|
||||
}
|
||||
|
||||
bool dialogue_silent_effect_get(const Schema& schema, const Element& entry, int index)
|
||||
{
|
||||
for (auto effectIndex : dialogue_effect_indices_get(schema, entry))
|
||||
{
|
||||
auto& effect = schema.elements[effectIndex];
|
||||
if (dialogue_effect_type_get(effect.typeString) == DialogueEffectType::SILENT && effect.start <= index &&
|
||||
index < effect.end)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int dialogue_effect_insert(Schema& schema, int entryIndex, int after, std::string_view type, int start, int end)
|
||||
{
|
||||
if (entryIndex < 0 || entryIndex >= (int)schema.elements.size()) return -1;
|
||||
@@ -1099,9 +1134,147 @@ namespace game::developer
|
||||
return isDirty;
|
||||
}
|
||||
|
||||
void animation_append_id_combo_draw(const Schema& schema, std::string& value)
|
||||
{
|
||||
auto preview = value.empty() ? "<none>" : value.c_str();
|
||||
if (ImGui::BeginCombo("AppendStringForAnimations", preview))
|
||||
{
|
||||
if (ImGui::Selectable("<none>", value.empty())) value.clear();
|
||||
|
||||
std::vector<std::string> options{};
|
||||
if (auto* root = schema.root())
|
||||
if (auto* stages = schema.child_get(*root, Element::STAGES))
|
||||
for (auto* stage : schema.children_get(*stages, Element::STAGE))
|
||||
{
|
||||
if (!stage || stage->animationAppendID.empty()) continue;
|
||||
if (std::ranges::find(options, stage->animationAppendID) != options.end()) continue;
|
||||
options.emplace_back(stage->animationAppendID);
|
||||
}
|
||||
|
||||
for (auto& option : options)
|
||||
if (ImGui::Selectable(option.c_str(), value == option)) value = option;
|
||||
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> animation_append_ids_get(const Schema& schema)
|
||||
{
|
||||
std::vector<std::string> result{};
|
||||
if (auto* root = schema.root())
|
||||
if (auto* stages = schema.child_get(*root, Element::STAGES))
|
||||
for (auto* stage : schema.children_get(*stages, Element::STAGE))
|
||||
{
|
||||
if (!stage || stage->animationAppendID.empty()) continue;
|
||||
if (std::ranges::find(result, stage->animationAppendID) != result.end()) continue;
|
||||
result.emplace_back(stage->animationAppendID);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool animation_name_appended_get(std::string_view name, const std::vector<std::string>& appendIDs)
|
||||
{
|
||||
for (auto& appendID : appendIDs)
|
||||
if (name.find(appendID) != std::string_view::npos) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> dialogue_animations_get(const ReferenceInfo* reference)
|
||||
{
|
||||
std::vector<std::string> result{};
|
||||
if (!reference || !reference->schema) return result;
|
||||
auto* root = reference->schema->root();
|
||||
if (!root || !root->anm2Entry.is_valid()) return result;
|
||||
|
||||
auto appendIDs = animation_append_ids_get(*reference->schema);
|
||||
for (auto& animation : root->anm2Entry.anm2.animations)
|
||||
{
|
||||
if (animation.name.empty() || animation_name_appended_get(animation.name, appendIDs)) continue;
|
||||
if (std::ranges::find(result, animation.name) != result.end()) continue;
|
||||
result.emplace_back(animation.name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool animation_combo_draw(const char* label, std::string& value, const std::vector<std::string>& animations)
|
||||
{
|
||||
auto isDirty = false;
|
||||
auto preview = value.empty() ? "<none>" : value.c_str();
|
||||
if (ImGui::BeginCombo(label, preview))
|
||||
{
|
||||
if (ImGui::Selectable("<none>", value.empty()))
|
||||
{
|
||||
value.clear();
|
||||
isDirty = true;
|
||||
}
|
||||
for (auto& animation : animations)
|
||||
{
|
||||
auto isSelected = value == animation;
|
||||
if (ImGui::Selectable(animation.c_str(), isSelected))
|
||||
{
|
||||
value = animation;
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
return isDirty;
|
||||
}
|
||||
|
||||
std::vector<std::string> sound_files_get(const std::filesystem::path& schemaPath, const std::string& rootPath)
|
||||
{
|
||||
std::vector<std::string> result{};
|
||||
auto root = resource_absolute_path_get(schemaPath, rootPath, {});
|
||||
std::error_code error{};
|
||||
if (!std::filesystem::is_directory(root, error)) return result;
|
||||
for (auto it = std::filesystem::recursive_directory_iterator(root, error);
|
||||
!error && it != std::filesystem::recursive_directory_iterator{}; it.increment(error))
|
||||
{
|
||||
if (!it->is_regular_file(error)) continue;
|
||||
auto extension = it->path().extension().string();
|
||||
std::ranges::transform(extension, extension.begin(), [](unsigned char character)
|
||||
{ return (char)std::tolower(character); });
|
||||
if (extension != ".wav" && extension != ".ogg" && extension != ".mp3") continue;
|
||||
auto relative = std::filesystem::relative(it->path(), root, error);
|
||||
if (!error && !relative.empty()) result.emplace_back(relative.generic_string());
|
||||
}
|
||||
std::ranges::sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool sound_combo_draw(const char* label, const std::filesystem::path& schemaPath, const std::string& rootPath,
|
||||
Element& effect)
|
||||
{
|
||||
auto isDirty = false;
|
||||
auto files = sound_files_get(schemaPath, rootPath);
|
||||
auto preview = effect.sound.empty() ? "<none>" : effect.sound.c_str();
|
||||
if (ImGui::BeginCombo(label, preview))
|
||||
{
|
||||
if (ImGui::Selectable("<none>", effect.sound.empty()))
|
||||
{
|
||||
effect.sound.clear();
|
||||
effect.soundEntry = {};
|
||||
isDirty = true;
|
||||
}
|
||||
for (auto& file : files)
|
||||
{
|
||||
auto isSelected = effect.sound == file;
|
||||
if (ImGui::Selectable(file.c_str(), isSelected))
|
||||
{
|
||||
effect.sound = file;
|
||||
sound_effect_entry_rebuild(schemaPath, rootPath, effect);
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
return isDirty;
|
||||
}
|
||||
|
||||
bool pool_entry_delete(Schema& schema, int poolIndex, int poolEntryIndex)
|
||||
{
|
||||
if (poolIndex < 0 || poolIndex >= (int)schema.elements.size()) return false;
|
||||
if (!editor_schema::element_index_is_valid(schema, poolEntryIndex)) return false;
|
||||
auto& children = schema.elements[poolIndex].children;
|
||||
auto it = std::ranges::find(children, poolEntryIndex);
|
||||
if (it == children.end()) return false;
|
||||
@@ -1183,9 +1356,10 @@ namespace game::developer
|
||||
}
|
||||
auto delayCharacter =
|
||||
dialogue_delay_character_get(schema, entry.text, editor.previewIndex - 1, editor.previewDelay);
|
||||
auto isSilent = dialogue_silent_effect_get(schema, entry, editor.previewIndex - 1);
|
||||
editor.previewEntity.isTalking = editor.isPlaying &&
|
||||
editor.previewIndex < util::imgui::utf8_char_count(entry.text) &&
|
||||
!delayCharacter.isDisableTalk;
|
||||
!delayCharacter.isDisableTalk && !isSilent;
|
||||
editor.previewEntity.update();
|
||||
|
||||
auto& canvas = editor.previewCanvas.render(resources, editor.previewEntity, size);
|
||||
@@ -1231,9 +1405,8 @@ namespace game::developer
|
||||
|
||||
auto referenceInfo = reference_info_get(loaded_schema_get(loadedSchemas, Element::CHARACTER));
|
||||
auto* reference = referenceInfo ? &*referenceInfo : nullptr;
|
||||
auto dialogueAnimations = dialogue_animations_get(reference);
|
||||
auto menuInfo = reference_info_get(loaded_schema_get(loadedSchemas, Element::MENU));
|
||||
previewBlip = root->soundBlip.get();
|
||||
if (!previewBlip && reference && reference->blipAudio) previewBlip = reference->blipAudio;
|
||||
nameColor = reference && reference->isHasColor ? reference->color : glm::vec3{NAME_COLOR_DEFAULT};
|
||||
|
||||
if (menuInfo && menuInfo->path && !menuInfo->font.empty() &&
|
||||
@@ -1278,6 +1451,7 @@ namespace game::developer
|
||||
auto available = ImGui::GetContentRegionAvail();
|
||||
auto leftWidth = std::max(LEFT_PANEL_MIN_WIDTH, available.x * LEFT_PANEL_WIDTH_RATIO);
|
||||
auto rightWidth = std::max(WIDGET_MIN_SIZE, available.x - leftWidth - ImGui::GetStyle().ItemSpacing.x);
|
||||
auto isDialogueStructureChanged = false;
|
||||
|
||||
ImGui::BeginChild("dialogue-left", ImVec2(leftWidth, 0.0f));
|
||||
auto settingsHeight = ImGui::GetFrameHeightWithSpacing();
|
||||
@@ -1285,7 +1459,7 @@ namespace game::developer
|
||||
if (ImGui::BeginChild("dialogue-settings", ImVec2(0.0f, settingsHeight), ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse))
|
||||
{
|
||||
util::imgui::input_string("AppendStringForAnimations", animationAppendID);
|
||||
animation_append_id_combo_draw(reference && reference->schema ? *reference->schema : schema, animationAppendID);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
@@ -1308,6 +1482,43 @@ namespace game::developer
|
||||
mode = ENTRY;
|
||||
scrollToEntry = selectedEntry;
|
||||
};
|
||||
auto entryNavigate = [&](int offset)
|
||||
{
|
||||
if (entries.empty()) return -1;
|
||||
auto it = std::ranges::find(entries, selectedEntry);
|
||||
if (it == entries.end()) return offset > 0 ? entries.front() : entries.back();
|
||||
auto index = (int)std::distance(entries.begin(), it);
|
||||
index = std::clamp(index + offset, 0, (int)entries.size() - 1);
|
||||
return entries[index];
|
||||
};
|
||||
auto entrySelectionApply = [&](int index, bool isToggle, bool isRange)
|
||||
{
|
||||
if (!element_is_type(schema, index, Element::ENTRY)) return;
|
||||
mode = ENTRY;
|
||||
if (isToggle)
|
||||
element_selection_toggle(selectedEntry, selectedEntries, index);
|
||||
else if (isRange)
|
||||
element_selection_range_set(schema, selectedEntry, selectedEntries, index);
|
||||
else
|
||||
element_selection_set(selectedEntry, selectedEntries, index);
|
||||
selectedPoolEntry = -1;
|
||||
selectedChoice = -1;
|
||||
selectedEffect = -1;
|
||||
textSelectionStart = 0;
|
||||
textSelectionEnd = 0;
|
||||
textSelectionStartByte = 0;
|
||||
textSelectionEndByte = 0;
|
||||
previewIndex = 0;
|
||||
previewTimer = 0.0f;
|
||||
previewBaseDelay = std::max(DELAY_MIN, delay);
|
||||
previewDelay = previewBaseDelay;
|
||||
isPreviewBlipQueued = false;
|
||||
auto& entry = schema.elements[index];
|
||||
isPlaying = util::imgui::utf8_char_count(entry.text) > 0;
|
||||
previewNeedsAnimationPlay = true;
|
||||
scrollToEntry = index;
|
||||
if (isPlaying) dialogue_preview_start_effects_apply(*this, schema, entry, previewIndex);
|
||||
};
|
||||
auto entryMenuDraw = [&](int target)
|
||||
{
|
||||
auto actionDirty = false;
|
||||
@@ -1335,8 +1546,8 @@ namespace game::developer
|
||||
selected_elements_copy(clipboard, schema, target, selectedEntries, Element::ENTRY);
|
||||
if (ImGui::MenuItem("Paste", "Ctrl+V", false, entriesParent >= 0))
|
||||
{
|
||||
auto pasted =
|
||||
elements_paste(clipboard, schema, entriesParent, sibling_after_get(schema, entriesParent, target));
|
||||
auto pasted = elements_paste(clipboard, schema, entriesParent,
|
||||
sibling_after_get(schema, entriesParent, target), Element::ENTRY);
|
||||
if (pasted >= 0)
|
||||
{
|
||||
element_selection_set(selectedEntry, selectedEntries, pasted);
|
||||
@@ -1353,6 +1564,7 @@ namespace game::developer
|
||||
{
|
||||
isDirty = true;
|
||||
entriesListChanged = true;
|
||||
isDialogueStructureChanged = true;
|
||||
}
|
||||
document_history_context_menu_draw();
|
||||
ImGui::EndPopup();
|
||||
@@ -1362,35 +1574,15 @@ namespace game::developer
|
||||
if (entriesListChanged) break;
|
||||
auto& entry = schema.elements[index];
|
||||
auto isSelected = mode == ENTRY && element_selected_has(selectedEntry, selectedEntries, index);
|
||||
ImGui::SetNextItemAllowOverlap();
|
||||
auto renameResult =
|
||||
util::imgui::selectable_renamable(std::format("entry-{}", index).c_str(), entry.idString, "<entry>",
|
||||
isSelected, ImGuiSelectableFlags_SelectOnRelease);
|
||||
isSelected);
|
||||
if (renameResult.selected && !ImGui::IsDragDropActive() &&
|
||||
!ImGui::IsMouseDragPastThreshold(ImGuiMouseButton_Left))
|
||||
{
|
||||
mode = ENTRY;
|
||||
auto& io = ImGui::GetIO();
|
||||
if (io.KeyCtrl)
|
||||
element_selection_toggle(selectedEntry, selectedEntries, index);
|
||||
else if (io.KeyShift)
|
||||
element_selection_range_set(schema, selectedEntry, selectedEntries, index);
|
||||
else
|
||||
element_selection_set(selectedEntry, selectedEntries, index);
|
||||
selectedPoolEntry = -1;
|
||||
selectedChoice = -1;
|
||||
selectedEffect = -1;
|
||||
textSelectionStart = 0;
|
||||
textSelectionEnd = 0;
|
||||
textSelectionStartByte = 0;
|
||||
textSelectionEndByte = 0;
|
||||
previewIndex = 0;
|
||||
previewTimer = 0.0f;
|
||||
previewBaseDelay = std::max(DELAY_MIN, delay);
|
||||
previewDelay = previewBaseDelay;
|
||||
isPreviewBlipQueued = false;
|
||||
isPlaying = util::imgui::utf8_char_count(entry.text) > 0;
|
||||
previewNeedsAnimationPlay = true;
|
||||
if (isPlaying) dialogue_preview_start_effects_apply(*this, schema, entry, previewIndex);
|
||||
entrySelectionApply(index, io.KeyCtrl, io.KeyShift);
|
||||
}
|
||||
if (renameResult.renamed)
|
||||
{
|
||||
@@ -1413,6 +1605,7 @@ namespace game::developer
|
||||
{
|
||||
isDirty = true;
|
||||
entriesListChanged = true;
|
||||
isDialogueStructureChanged = true;
|
||||
}
|
||||
document_history_context_menu_draw();
|
||||
ImGui::EndPopup();
|
||||
@@ -1425,7 +1618,17 @@ namespace game::developer
|
||||
auto target = hoveredEntry >= 0 ? hoveredEntry : selectedEntry;
|
||||
if (element_is_type(schema, target, Element::ENTRY))
|
||||
{
|
||||
if (editor_schema::shortcut_is_pressed(ImGuiKey_Delete))
|
||||
if (editor_schema::shortcut_is_pressed(ImGuiKey_DownArrow))
|
||||
{
|
||||
auto next = entryNavigate(1);
|
||||
if (next >= 0) entrySelectionApply(next, ImGui::GetIO().KeyCtrl, ImGui::GetIO().KeyShift);
|
||||
}
|
||||
else if (editor_schema::shortcut_is_pressed(ImGuiKey_UpArrow))
|
||||
{
|
||||
auto previous = entryNavigate(-1);
|
||||
if (previous >= 0) entrySelectionApply(previous, ImGui::GetIO().KeyCtrl, ImGui::GetIO().KeyShift);
|
||||
}
|
||||
else if (editor_schema::shortcut_is_pressed(ImGuiKey_Delete))
|
||||
{
|
||||
if (!element_selected_has(selectedEntry, selectedEntries, target))
|
||||
element_selection_set(selectedEntry, selectedEntries, target);
|
||||
@@ -1447,7 +1650,7 @@ namespace game::developer
|
||||
else if (editor_schema::shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_V))
|
||||
{
|
||||
auto pasted = elements_paste(clipboard, schema, entriesParent,
|
||||
sibling_after_get(schema, entriesParent, target));
|
||||
sibling_after_get(schema, entriesParent, target), Element::ENTRY);
|
||||
if (pasted >= 0)
|
||||
{
|
||||
element_selection_set(selectedEntry, selectedEntries, pasted);
|
||||
@@ -1455,7 +1658,11 @@ namespace game::developer
|
||||
entriesListChanged = true;
|
||||
}
|
||||
}
|
||||
if (entriesListChanged) isDirty = true;
|
||||
if (entriesListChanged)
|
||||
{
|
||||
isDirty = true;
|
||||
isDialogueStructureChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1505,8 +1712,8 @@ namespace game::developer
|
||||
selected_elements_copy(clipboard, schema, target, selectedPools, Element::POOL);
|
||||
if (ImGui::MenuItem("Paste", "Ctrl+V", false, poolsParent >= 0))
|
||||
{
|
||||
auto pasted =
|
||||
elements_paste(clipboard, schema, poolsParent, sibling_after_get(schema, poolsParent, target));
|
||||
auto pasted = elements_paste(clipboard, schema, poolsParent,
|
||||
sibling_after_get(schema, poolsParent, target), Element::POOL);
|
||||
if (pasted >= 0)
|
||||
{
|
||||
element_selection_set(selectedPool, selectedPools, pasted);
|
||||
@@ -1523,6 +1730,8 @@ namespace game::developer
|
||||
{
|
||||
isDirty = true;
|
||||
poolsListChanged = true;
|
||||
selectedPoolEntry = -1;
|
||||
isDialogueStructureChanged = true;
|
||||
}
|
||||
document_history_context_menu_draw();
|
||||
ImGui::EndPopup();
|
||||
@@ -1532,9 +1741,10 @@ namespace game::developer
|
||||
if (poolsListChanged) break;
|
||||
auto& pool = schema.elements[index];
|
||||
auto isSelected = mode == POOL && element_selected_has(selectedPool, selectedPools, index);
|
||||
ImGui::SetNextItemAllowOverlap();
|
||||
auto renameResult =
|
||||
util::imgui::selectable_renamable(std::format("pool-{}", index).c_str(), pool.idString, "<pool>",
|
||||
isSelected, ImGuiSelectableFlags_SelectOnRelease);
|
||||
isSelected);
|
||||
if (renameResult.selected && !ImGui::IsDragDropActive() &&
|
||||
!ImGui::IsMouseDragPastThreshold(ImGuiMouseButton_Left))
|
||||
{
|
||||
@@ -1568,6 +1778,8 @@ namespace game::developer
|
||||
{
|
||||
isDirty = true;
|
||||
poolsListChanged = true;
|
||||
selectedPoolEntry = -1;
|
||||
isDialogueStructureChanged = true;
|
||||
}
|
||||
document_history_context_menu_draw();
|
||||
ImGui::EndPopup();
|
||||
@@ -1600,8 +1812,8 @@ namespace game::developer
|
||||
}
|
||||
else if (editor_schema::shortcut_is_pressed(ImGuiMod_Ctrl | ImGuiKey_V))
|
||||
{
|
||||
auto pasted =
|
||||
elements_paste(clipboard, schema, poolsParent, sibling_after_get(schema, poolsParent, target));
|
||||
auto pasted = elements_paste(clipboard, schema, poolsParent,
|
||||
sibling_after_get(schema, poolsParent, target), Element::POOL);
|
||||
if (pasted >= 0)
|
||||
{
|
||||
element_selection_set(selectedPool, selectedPools, pasted);
|
||||
@@ -1609,7 +1821,12 @@ namespace game::developer
|
||||
poolsListChanged = true;
|
||||
}
|
||||
}
|
||||
if (poolsListChanged) isDirty = true;
|
||||
if (poolsListChanged)
|
||||
{
|
||||
isDirty = true;
|
||||
selectedPoolEntry = -1;
|
||||
isDialogueStructureChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1626,7 +1843,11 @@ namespace game::developer
|
||||
|
||||
ImGui::PushID("dialogue-right");
|
||||
ImGui::BeginGroup();
|
||||
if (mode == ENTRY)
|
||||
if (isDialogueStructureChanged)
|
||||
{
|
||||
ImGui::TextUnformatted("Selection changed.");
|
||||
}
|
||||
else if (mode == ENTRY)
|
||||
{
|
||||
auto* entry = selected_get(schema, selectedEntry);
|
||||
if (!entry)
|
||||
@@ -1781,6 +2002,8 @@ namespace game::developer
|
||||
auto textEditorRectMax = ImGui::GetItemRectMax();
|
||||
|
||||
auto textEditorRightClicked = ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right);
|
||||
auto isTextEditorContextOpen =
|
||||
ImGui::IsPopupOpen("text-editor-context") || ImGui::IsPopupOpen("text-editor-item-context");
|
||||
auto textEditorScroll = ImVec2{};
|
||||
if (auto* inputState = ImGui::GetInputTextState(ImGui::GetItemID()))
|
||||
{
|
||||
@@ -1795,13 +2018,14 @@ namespace game::developer
|
||||
textSelectionStart = util::imgui::utf8_byte_to_char_index(entry->text, selectionStart);
|
||||
textSelectionEnd = util::imgui::utf8_byte_to_char_index(entry->text, selectionEnd);
|
||||
}
|
||||
else if (textEditorRightClicked && textSelectionStartByte != textSelectionEndByte)
|
||||
else if ((textEditorRightClicked || isTextEditorContextOpen) &&
|
||||
textSelectionStartByte != textSelectionEndByte)
|
||||
{
|
||||
inputState->WantReloadUserBuf = true;
|
||||
inputState->ReloadSelectionStart = textSelectionStartByte;
|
||||
inputState->ReloadSelectionEnd = textSelectionEndByte;
|
||||
}
|
||||
else if (!textEditorRightClicked)
|
||||
else if (!textEditorRightClicked && !isTextEditorContextOpen)
|
||||
{
|
||||
textSelectionStartByte = selectionStart;
|
||||
textSelectionEndByte = selectionEnd;
|
||||
@@ -1809,9 +2033,7 @@ namespace game::developer
|
||||
textSelectionEnd = util::imgui::utf8_byte_to_char_index(entry->text, selectionEnd);
|
||||
}
|
||||
}
|
||||
if ((textEditorRightClicked || ImGui::IsPopupOpen("text-editor-context") ||
|
||||
ImGui::IsPopupOpen("text-editor-item-context")) &&
|
||||
textSelectionStartByte != textSelectionEndByte)
|
||||
if ((textEditorRightClicked || isTextEditorContextOpen) && textSelectionStartByte != textSelectionEndByte)
|
||||
text_selection_overlay_draw(entry->text, textSelectionStartByte, textSelectionEndByte, textEditorRectMin,
|
||||
textEditorRectMax, textEditorScroll);
|
||||
|
||||
@@ -1878,7 +2100,8 @@ namespace game::developer
|
||||
if (entry_combo_draw("Next", schema, entry->next)) isDirty = true;
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Separator();
|
||||
if (util::imgui::input_string("Animation", entry->animation))
|
||||
ImGui::SetNextItemWidth(itemWidth);
|
||||
if (animation_combo_draw("Animation", entry->animation, dialogueAnimations))
|
||||
{
|
||||
isDirty = true;
|
||||
}
|
||||
@@ -1995,7 +2218,7 @@ namespace game::developer
|
||||
{
|
||||
case DialogueEffectType::ANIMATION:
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
if (util::imgui::input_string("##Value", child.animation)) isEffectsChanged = true;
|
||||
if (animation_combo_draw("##Value", child.animation, dialogueAnimations)) isEffectsChanged = true;
|
||||
break;
|
||||
case DialogueEffectType::DELAY:
|
||||
ImGui::SetNextItemWidth(-FLT_MIN);
|
||||
@@ -2003,6 +2226,7 @@ namespace game::developer
|
||||
break;
|
||||
case DialogueEffectType::OSCILLATION:
|
||||
case DialogueEffectType::SHAKE:
|
||||
case DialogueEffectType::SILENT:
|
||||
case DialogueEffectType::BIG:
|
||||
case DialogueEffectType::SMALL:
|
||||
break;
|
||||
@@ -2014,16 +2238,11 @@ namespace game::developer
|
||||
auto rootPath = root ? root->soundRootPath : std::string{};
|
||||
if (!child.sound.empty() && child.soundEntry.path != child.sound)
|
||||
sound_effect_entry_rebuild(schemaPath, rootPath, child);
|
||||
auto displayPath = resource_display_path_get(schemaPath, rootPath, child.sound);
|
||||
auto suffixWidth = SOUND_WIDGET_WIDTH;
|
||||
auto pathWidth = std::max(WIDGET_MIN_SIZE, ImGui::GetContentRegionAvail().x - suffixWidth -
|
||||
ImGui::GetStyle().ItemSpacing.x);
|
||||
if (util::imgui::path_widget("##Sound Value", child.sound, displayPath, ImVec2(pathWidth, 0.0f)))
|
||||
{
|
||||
pendingSetting = PENDING_SOUND;
|
||||
pendingSoundEffect = childIndex;
|
||||
dialog.file_open(util::Dialog::SCHEMA_SOUND_OPEN);
|
||||
}
|
||||
ImGui::SetNextItemWidth(pathWidth);
|
||||
if (sound_combo_draw("##Sound Value", schemaPath, rootPath, child)) isEffectsChanged = true;
|
||||
ImGui::SameLine();
|
||||
auto isMissing = !child.sound.empty() && !child.soundEntry.sound.is_valid();
|
||||
if (util::imgui::sound_widget("##Sound Play", isMissing))
|
||||
@@ -2141,6 +2360,7 @@ namespace game::developer
|
||||
case DialogueEffectType::OSCILLATION:
|
||||
case DialogueEffectType::SHAKE:
|
||||
case DialogueEffectType::SCREEN_SHAKE:
|
||||
case DialogueEffectType::SILENT:
|
||||
case DialogueEffectType::BIG:
|
||||
case DialogueEffectType::SMALL:
|
||||
break;
|
||||
@@ -2188,14 +2408,18 @@ namespace game::developer
|
||||
previewDelay = previewBaseDelay;
|
||||
dialogue_preview_start_effects_apply(*this, schema, *entry, previewIndex);
|
||||
auto delayCharacter = dialogue_delay_character_get(schema, entry->text, previewIndex - 1, previewDelay);
|
||||
auto isSilent = dialogue_silent_effect_get(schema, *entry, previewIndex - 1);
|
||||
previewDelay = delayCharacter.delay;
|
||||
auto isBlipStep = blipDelay > 0 && previewIndex % blipDelay == 0;
|
||||
if (delayCharacter.isDisableTalk)
|
||||
if (isSilent)
|
||||
isPreviewBlipQueued = false;
|
||||
else if (delayCharacter.isDisableTalk)
|
||||
{
|
||||
if (isBlipStep) isPreviewBlipQueued = true;
|
||||
}
|
||||
else if (isBlipStep || isPreviewBlipQueued)
|
||||
{
|
||||
auto* previewBlip = preview_blip_get(schema, reference);
|
||||
if (previewBlip && previewBlip->is_valid()) previewBlip->play();
|
||||
isPreviewBlipQueued = false;
|
||||
}
|
||||
@@ -2229,6 +2453,7 @@ namespace game::developer
|
||||
}
|
||||
else if (ImGui::BeginChild("pool-entries", ImVec2(rightWidth, 0.0f), ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto isPoolEntriesChanged = false;
|
||||
auto poolEntriesShortcutScope = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) ||
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows);
|
||||
auto hoveredPoolEntry = -1;
|
||||
@@ -2249,6 +2474,7 @@ namespace game::developer
|
||||
if (pool_entry_delete(schema, selectedPool, selectedPoolEntry))
|
||||
{
|
||||
selectedPoolEntry = -1;
|
||||
isPoolEntriesChanged = true;
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
@@ -2258,10 +2484,12 @@ namespace game::developer
|
||||
auto children = pool->children;
|
||||
for (auto childIndex : children)
|
||||
{
|
||||
if (isPoolEntriesChanged) break;
|
||||
if (childIndex < 0 || childIndex >= (int)schema.elements.size()) continue;
|
||||
auto& poolEntry = schema.elements[childIndex];
|
||||
if (poolEntry.type != Element::POOL_ENTRY) continue;
|
||||
auto label = poolEntry.idString.empty() ? "<entry>" : poolEntry.idString.c_str();
|
||||
ImGui::SetNextItemAllowOverlap();
|
||||
if (ImGui::Selectable(label, selectedPoolEntry == childIndex)) selectedPoolEntry = childIndex;
|
||||
scroll_to_item_if_requested(scrollToPoolEntry, childIndex);
|
||||
if (ImGui::IsItemHovered()) hoveredPoolEntry = childIndex;
|
||||
@@ -2280,15 +2508,17 @@ namespace game::developer
|
||||
if (pool_entry_delete(schema, selectedPool, childIndex))
|
||||
{
|
||||
selectedPoolEntry = -1;
|
||||
isPoolEntriesChanged = true;
|
||||
isDirty = true;
|
||||
}
|
||||
}
|
||||
document_history_context_menu_draw();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
if (isPoolEntriesChanged) break;
|
||||
if (auto* entry = schema.dialogue_entry_get(poolEntry.idString)) entry_tooltip_draw(schema, *entry);
|
||||
}
|
||||
if (poolEntriesShortcutScope && editor_schema::shortcut_is_pressed(ImGuiKey_Delete))
|
||||
if (!isPoolEntriesChanged && poolEntriesShortcutScope && editor_schema::shortcut_is_pressed(ImGuiKey_Delete))
|
||||
{
|
||||
auto target = hoveredPoolEntry >= 0 ? hoveredPoolEntry : selectedPoolEntry;
|
||||
if (target >= 0 && pool_entry_delete(schema, selectedPool, target))
|
||||
|
||||
@@ -383,7 +383,6 @@ namespace game::developer
|
||||
resource::xml::Character previewCharacter{};
|
||||
util::Clipboard clipboard{};
|
||||
resource::Font previewFont{};
|
||||
const resource::Audio* previewBlip{};
|
||||
Entity previewEntity{};
|
||||
util::imgui::widget::EntityCanvas previewCanvas{};
|
||||
std::string animationAppendID{};
|
||||
|
||||
+10
-33
@@ -22,7 +22,6 @@ namespace game::developer
|
||||
constexpr auto RECENT_FILES_MAX = 10;
|
||||
constexpr auto CENTER_RATIO = 0.5f;
|
||||
constexpr auto DEVELOPER_WINDOW_SIZE_RATIO = 0.75f;
|
||||
constexpr auto INFO_SCHEMA_TABLE_COLUMNS = 4;
|
||||
constexpr auto DEVELOPER_WINDOW_FLAGS = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings;
|
||||
constexpr auto EDITOR_CHILD_FLAGS = ImGuiWindowFlags_MenuBar;
|
||||
constexpr auto DIALOGUE_SCHEMA_FILENAME = "dialogue.xml";
|
||||
@@ -338,11 +337,6 @@ namespace game::developer
|
||||
selectedDocument = i;
|
||||
if (ImGui::BeginTabBar("##Document Tabs"))
|
||||
{
|
||||
if (ImGui::BeginTabItem("Info"))
|
||||
{
|
||||
document_info_update(documents[i]);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
if (ImGui::BeginTabItem("Dialogue"))
|
||||
{
|
||||
document_dialogue_update(resources, documents[i]);
|
||||
@@ -359,33 +353,6 @@ namespace game::developer
|
||||
if (closeRequest >= 0) close(closeRequest);
|
||||
}
|
||||
|
||||
void Window::document_info_update(const Document& document)
|
||||
{
|
||||
if (!ImGui::BeginTable("##Schema Info", INFO_SCHEMA_TABLE_COLUMNS, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg))
|
||||
return;
|
||||
|
||||
ImGui::TableSetupColumn("Schema");
|
||||
ImGui::TableSetupColumn("File");
|
||||
ImGui::TableSetupColumn("Exists");
|
||||
ImGui::TableSetupColumn("Valid");
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
for (auto& schema : document.schemas)
|
||||
{
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(schema.label.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(schema.filename.c_str());
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(schema.isExists ? "Yes" : "No");
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextUnformatted(schema.isValid ? "Yes" : "No");
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
void Window::document_dialogue_update(Resources& resources, Document& document)
|
||||
{
|
||||
auto* dialogue = document.dialogue_schema_get();
|
||||
@@ -450,6 +417,16 @@ namespace game::developer
|
||||
void Window::update(Resources& resources, SDL_Window* window, state::Play* play)
|
||||
{
|
||||
dialog.window_set(window);
|
||||
auto* settingsRoot = resources.settings.root();
|
||||
|
||||
if (!settingsRoot || !settingsRoot->isDeveloperConsoleEnabled)
|
||||
{
|
||||
resources.isDeveloper = false;
|
||||
isDeveloperOpen = false;
|
||||
action = {};
|
||||
console.action = {};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resources.isDeveloper && shortcut_is_pressed(ImGuiKey_GraveAccent))
|
||||
{
|
||||
|
||||
@@ -48,7 +48,6 @@ namespace game::developer
|
||||
void editor_shortcuts_update();
|
||||
void editor_menu_update();
|
||||
void editor_documents_update(Resources&);
|
||||
void document_info_update(const Document&);
|
||||
void document_dialogue_update(Resources&, Document&);
|
||||
void dialog_selection_update();
|
||||
void recent_files_load();
|
||||
|
||||
+123
-42
@@ -18,6 +18,19 @@
|
||||
namespace game
|
||||
{
|
||||
constexpr auto CHARACTER_VALUE_ZERO = 0.0f;
|
||||
constexpr auto MODEL_IDENTITY = 1.0f;
|
||||
constexpr auto DEBUG_COLOR_MIN = 0.0f;
|
||||
constexpr auto DEBUG_COLOR_MAX = 1.0f;
|
||||
constexpr auto DEBUG_LAYER_COLOR = glm::vec4(DEBUG_COLOR_MAX, DEBUG_COLOR_MIN, DEBUG_COLOR_MIN, DEBUG_COLOR_MAX);
|
||||
constexpr auto DEBUG_NULL_COLOR = glm::vec4(DEBUG_COLOR_MIN, DEBUG_COLOR_MIN, DEBUG_COLOR_MAX, DEBUG_COLOR_MAX);
|
||||
|
||||
struct Anm2RenderState
|
||||
{
|
||||
glm::mat4 model{MODEL_IDENTITY};
|
||||
glm::vec4 tint{MODEL_IDENTITY};
|
||||
glm::vec3 colorOffset{};
|
||||
bool isVisible{true};
|
||||
};
|
||||
|
||||
float value_clamp(float value, float minimum, float maximum)
|
||||
{
|
||||
@@ -34,6 +47,41 @@ namespace game
|
||||
return std::clamp(value, rangeMin, rangeMax);
|
||||
}
|
||||
|
||||
static glm::mat4 anm2_root_model_get(const Entity::Anm2::Frame& frame, glm::vec2 position = {})
|
||||
{
|
||||
return util::math::quad_model_no_size_get(frame.position + position, frame.pivot,
|
||||
util::math::to_unit(frame.scale), frame.rotation);
|
||||
}
|
||||
|
||||
static Anm2RenderState anm2_group_state_get(Entity& entity, Entity::Anm2::Animation& animation, int groupID,
|
||||
float frameTime)
|
||||
{
|
||||
Anm2RenderState state{};
|
||||
if (!util::vector::in_bounds(animation.groups, groupID)) return state;
|
||||
|
||||
auto& group = animation.groups[groupID];
|
||||
state = anm2_group_state_get(entity, animation, group.parent, frameTime);
|
||||
if (!state.isVisible) return state;
|
||||
if (!group.isVisible)
|
||||
{
|
||||
state.isVisible = false;
|
||||
return state;
|
||||
}
|
||||
if (group.rootAnimation.frames.empty()) return state;
|
||||
|
||||
auto frame = entity.frame_generate(group.rootAnimation, frameTime, Entity::Anm2::GROUP, groupID);
|
||||
if (!frame.isVisible)
|
||||
{
|
||||
state.isVisible = false;
|
||||
return state;
|
||||
}
|
||||
|
||||
state.model *= anm2_root_model_get(frame);
|
||||
state.tint *= frame.tint;
|
||||
state.colorOffset += frame.colorOffset;
|
||||
return state;
|
||||
}
|
||||
|
||||
Entity::Override::Override(int _id, Entity::Anm2::Type _type, Entity::Override::Mode _mode,
|
||||
Entity::FrameOptional _frame, std::optional<float> _time,
|
||||
Entity::Override::Function _function, float _cycles)
|
||||
@@ -86,28 +134,27 @@ namespace game
|
||||
Entity::Anm2::Animation* Entity::animation_get(int index)
|
||||
{
|
||||
if (index == -1) index = animationIndex;
|
||||
if (animationMapReverse.contains(index)) return &animations[index];
|
||||
if (index >= 0 && index < (int)animations.size() && animationMapReverse.contains(index)) return &animations[index];
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Entity::Anm2::Animation* Entity::animation_get(const std::string& name)
|
||||
{
|
||||
if (animationMap.contains(name)) return &animations[animationMap[name]];
|
||||
return nullptr;
|
||||
auto it = animationMap.find(name);
|
||||
return it == animationMap.end() ? nullptr : animation_get(it->second);
|
||||
}
|
||||
|
||||
bool Entity::is_playing(const std::string& name)
|
||||
{
|
||||
if (name.empty())
|
||||
return state == PLAYING;
|
||||
else
|
||||
return state == PLAYING && animationMap[name] == animationIndex;
|
||||
if (name.empty()) return state == PLAYING;
|
||||
auto it = animationMap.find(name);
|
||||
return it != animationMap.end() && state == PLAYING && it->second == animationIndex;
|
||||
}
|
||||
|
||||
int Entity::animation_index_get(const std::string& name)
|
||||
{
|
||||
if (animationMap.contains(name)) return animationMap[name];
|
||||
return -1;
|
||||
auto it = animationMap.find(name);
|
||||
return it == animationMap.end() ? -1 : it->second;
|
||||
}
|
||||
|
||||
Entity::Anm2::Item* Entity::item_get(Entity::Anm2::Type type, int id, int checkAnimationIndex)
|
||||
@@ -374,11 +421,13 @@ namespace game
|
||||
auto frame = frame_generate(*item, checkTime, Entity::Anm2::NULL_, nullID);
|
||||
if (!frame.isVisible) return glm::vec4(NAN);
|
||||
|
||||
auto rootModel = util::math::quad_model_no_size_get(root.position + position, root.pivot,
|
||||
util::math::to_unit(root.scale), root.rotation);
|
||||
auto groupState = anm2_group_state_get(*this, *animation, item->groupID, checkTime);
|
||||
if (!groupState.isVisible) return glm::vec4(NAN);
|
||||
|
||||
auto rootModel = anm2_root_model_get(root, position);
|
||||
auto frameModel =
|
||||
util::math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, glm::vec2(1.0f), frame.rotation);
|
||||
auto model = rootModel * frameModel;
|
||||
auto model = rootModel * groupState.model * frameModel;
|
||||
|
||||
float minX = std::numeric_limits<float>::infinity();
|
||||
float minY = std::numeric_limits<float>::infinity();
|
||||
@@ -406,14 +455,16 @@ namespace game
|
||||
|
||||
auto root = frame_generate(animation->rootAnimation, time, Entity::Anm2::ROOT);
|
||||
|
||||
auto rootModel = util::math::quad_model_no_size_get(root.position + position, root.pivot,
|
||||
util::math::to_unit(root.scale), root.rotation);
|
||||
auto rootModel = anm2_root_model_get(root, position);
|
||||
|
||||
for (auto& i : animation->layerOrder)
|
||||
{
|
||||
auto& layerAnimation = animation->layerAnimations[i];
|
||||
if (!layerAnimation.isVisible) continue;
|
||||
|
||||
auto groupState = anm2_group_state_get(*this, *animation, layerAnimation.groupID, time);
|
||||
if (!groupState.isVisible) continue;
|
||||
|
||||
auto layer = util::map::find(layers, i);
|
||||
if (!layer) continue;
|
||||
|
||||
@@ -425,13 +476,13 @@ namespace game
|
||||
|
||||
auto model = util::math::quad_model_get(frame.size, frame.position, frame.pivot, util::math::to_unit(frame.scale),
|
||||
frame.rotation);
|
||||
model = rootModel * model;
|
||||
model = rootModel * groupState.model * model;
|
||||
|
||||
auto& texture = spritesheet->texture;
|
||||
if (!texture.is_valid()) return;
|
||||
|
||||
auto tint = frame.tint * root.tint;
|
||||
auto colorOffset = frame.colorOffset + root.colorOffset;
|
||||
auto tint = frame.tint * groupState.tint * root.tint;
|
||||
auto colorOffset = frame.colorOffset + groupState.colorOffset + root.colorOffset;
|
||||
|
||||
auto inset = glm::vec2(0);
|
||||
auto uvMin = (frame.crop + inset) / glm::vec2(texture.size);
|
||||
@@ -439,23 +490,37 @@ namespace game
|
||||
auto uvVertices = util::math::uv_vertices_get(uvMin, uvMax);
|
||||
|
||||
canvas.texture_render(textureShader, texture.id, model, tint, colorOffset, uvVertices.data());
|
||||
if (isShowLayers) canvas.rect_render(rectShader, model, DEBUG_LAYER_COLOR);
|
||||
}
|
||||
|
||||
if (isShowNulls)
|
||||
{
|
||||
for (int i = 0; i < (int)animation->nullAnimations.size(); i++)
|
||||
auto null_render = [&](int i, Entity::Anm2::Item& nullAnimation)
|
||||
{
|
||||
auto& nullAnimation = animation->nullAnimations[i];
|
||||
if (!nullAnimation.isVisible) continue;
|
||||
if (!nullAnimation.isVisible) return;
|
||||
|
||||
auto groupState = anm2_group_state_get(*this, *animation, nullAnimation.groupID, time);
|
||||
if (!groupState.isVisible) return;
|
||||
|
||||
auto frame = frame_generate(nullAnimation, time, Entity::Anm2::NULL_, i);
|
||||
if (!frame.isVisible) continue;
|
||||
if (!frame.isVisible) return;
|
||||
|
||||
auto model = util::math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, glm::vec2(1.0f),
|
||||
frame.rotation);
|
||||
model = rootModel * model;
|
||||
model = rootModel * groupState.model * model;
|
||||
|
||||
canvas.rect_render(rectShader, model);
|
||||
canvas.rect_render(rectShader, model, DEBUG_NULL_COLOR);
|
||||
};
|
||||
|
||||
if (!animation->nullOrder.empty())
|
||||
{
|
||||
for (auto& i : animation->nullOrder)
|
||||
if (auto nullAnimation = util::map::find(animation->nullAnimations, i)) null_render(i, *nullAnimation);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto& [i, nullAnimation] : animation->nullAnimations)
|
||||
null_render(i, nullAnimation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,19 +544,22 @@ namespace game
|
||||
glm::mat4 transform(1.0f);
|
||||
|
||||
auto root = frame_generate(animation->rootAnimation, t, Entity::Anm2::ROOT);
|
||||
transform *= util::math::quad_model_no_size_get(root.position + position, root.pivot,
|
||||
util::math::to_unit(root.scale), root.rotation);
|
||||
transform *= anm2_root_model_get(root, position);
|
||||
|
||||
for (auto& [id, layerAnimation] : animation->layerAnimations)
|
||||
{
|
||||
if (!layerAnimation.isVisible) continue;
|
||||
|
||||
auto groupState = anm2_group_state_get(*this, *animation, layerAnimation.groupID, t);
|
||||
if (!groupState.isVisible) continue;
|
||||
|
||||
auto frame = frame_generate(layerAnimation, t, Entity::Anm2::LAYER, id);
|
||||
|
||||
if (frame.size == glm::vec2() || !frame.isVisible) continue;
|
||||
|
||||
auto layerTransform = transform * util::math::quad_model_get(frame.size, frame.position, frame.pivot,
|
||||
util::math::to_unit(frame.scale), frame.rotation);
|
||||
auto layerTransform = transform * groupState.model *
|
||||
util::math::quad_model_get(frame.size, frame.position, frame.pivot,
|
||||
util::math::to_unit(frame.scale), frame.rotation);
|
||||
for (auto& corner : CORNERS)
|
||||
{
|
||||
glm::vec4 world = layerTransform * glm::vec4(corner, 0.0f, 1.0f);
|
||||
@@ -575,7 +643,6 @@ namespace game
|
||||
capacity = saveCharacter ? saveCharacter->capacityCalories : (double)root->capacityCalories;
|
||||
weight = saveCharacter ? saveCharacter->weightKilograms : (double)root->weightKilograms;
|
||||
digestionRate = saveCharacter ? saveCharacter->digestionRate : (double)root->digestionRate;
|
||||
eatSpeed = saveCharacter ? saveCharacter->eatSpeedMultiplier : (double)root->eatSpeedMultiplier;
|
||||
|
||||
calories = saveCharacter ? saveCharacter->calories : 0;
|
||||
|
||||
@@ -713,12 +780,13 @@ namespace game
|
||||
weight = value_clamp(weight, root->weightKilograms, root->weightMaxKilograms);
|
||||
capacity = value_clamp(capacity, root->capacityMinCalories, root->capacityMaxCalories);
|
||||
digestionRate = value_clamp(digestionRate, root->digestionRateMin, root->digestionRateMax);
|
||||
eatSpeed = value_clamp(eatSpeed, root->eatSpeedMinMultiplier, root->eatSpeedMaxMultiplier);
|
||||
calories = value_clamp(calories, CHARACTER_VALUE_ZERO, std::max(CHARACTER_VALUE_ZERO, max_capacity()));
|
||||
digestionProgress = value_clamp(digestionProgress, CHARACTER_VALUE_ZERO, DIGESTION_MAX);
|
||||
digestionTimer = value_clamp(digestionTimer, 0, std::max(0, root->digestionTimerMax));
|
||||
totalCaloriesConsumed = std::max(CHARACTER_VALUE_ZERO, totalCaloriesConsumed);
|
||||
totalFoodItemsEaten = std::max(0, totalFoodItemsEaten);
|
||||
totalItemsSpawned = std::max(0, totalItemsSpawned);
|
||||
totalItemsConsumed = std::max(0, totalItemsConsumed);
|
||||
totalPlaytimeSeconds = std::max(CHARACTER_VALUE_ZERO, totalPlaytimeSeconds);
|
||||
stage = value_clamp(stage_get(), 0, stage_max_get());
|
||||
}
|
||||
|
||||
@@ -801,7 +869,6 @@ namespace game
|
||||
(double)data.root()->capacityMaxCalories);
|
||||
}
|
||||
|
||||
totalCaloriesConsumed += calories;
|
||||
calories = 0;
|
||||
|
||||
if (auto nextStage = stage_from_weight_get((double)nextWeight); nextStage > stage_from_weight_get(weight))
|
||||
@@ -844,7 +911,28 @@ namespace game
|
||||
stage = stage_get();
|
||||
expand_areas_apply();
|
||||
|
||||
if (talkOverrideID < 0 || talkOverrideID >= (int)overrides.size() || blinkOverrideID < 0 ||
|
||||
blinkOverrideID >= (int)overrides.size())
|
||||
return;
|
||||
|
||||
auto* animation = animation_get();
|
||||
auto isAnimationStateValid = animation && animationIndex >= 0 &&
|
||||
animationIndex < (int)animationTalkDurations.size() &&
|
||||
animationIndex < (int)animationBlinkDurations.size();
|
||||
|
||||
auto& talkOverride = overrides[talkOverrideID];
|
||||
auto& blinkOverride = overrides[blinkOverrideID];
|
||||
|
||||
if (!isAnimationStateValid)
|
||||
{
|
||||
isTalking = false;
|
||||
isBlinking = false;
|
||||
talkTimer = 0.0f;
|
||||
blinkTimer = 0.0f;
|
||||
talkOverride.frame = {};
|
||||
blinkOverride.frame = {};
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTalking)
|
||||
{
|
||||
@@ -858,7 +946,7 @@ namespace game
|
||||
auto* talk = data.overrides_root() ? data.child_get(*data.overrides_root(), resource::xml::Schema::Element::TALK)
|
||||
: nullptr;
|
||||
auto id = talk ? data.layer_id_get(talk->layerSource) : -1;
|
||||
auto& layerAnimations = animation_get()->layerAnimations;
|
||||
auto& layerAnimations = animation->layerAnimations;
|
||||
|
||||
if (layerAnimations.contains(id) && animationTalkDurations.at(animationIndex) > -1)
|
||||
{
|
||||
@@ -885,8 +973,6 @@ namespace game
|
||||
else
|
||||
talkOverride.frame = {};
|
||||
|
||||
auto& blinkOverride = overrides[blinkOverrideID];
|
||||
|
||||
if (auto blinkDuration = animationBlinkDurations[animationIndex]; blinkDuration != 1)
|
||||
{
|
||||
if (util::math::random_percent_roll(data.root()->blinkChance)) isBlinking = true;
|
||||
@@ -904,7 +990,7 @@ namespace game
|
||||
? data.child_get(*data.overrides_root(), resource::xml::Schema::Element::BLINK)
|
||||
: nullptr;
|
||||
auto id = blink ? data.layer_id_get(blink->layerSource) : -1;
|
||||
auto& layerAnimations = animation_get()->layerAnimations;
|
||||
auto& layerAnimations = animation->layerAnimations;
|
||||
|
||||
if (layerAnimations.contains(id))
|
||||
{
|
||||
@@ -958,21 +1044,16 @@ namespace game
|
||||
spritesheetType = type;
|
||||
}
|
||||
|
||||
Entity::Entity(Entity::Anm2 _anm2, glm::ivec2 _position, int _schemaID, int _durability, int _animationIndex,
|
||||
glm::vec2 _velocity, float _rotation)
|
||||
Entity::Entity(Entity::Anm2 _anm2, glm::ivec2 _position, int _schemaID, glm::vec2 _velocity, float _rotation)
|
||||
{
|
||||
anm2_set(_anm2);
|
||||
position = _position;
|
||||
schemaID = _schemaID;
|
||||
durability = _durability;
|
||||
velocity = _velocity;
|
||||
entityType = ITEM;
|
||||
mode = SET;
|
||||
startTime = 0.0f;
|
||||
if (_animationIndex != -1)
|
||||
play({.index = _animationIndex, .mode = SET});
|
||||
else
|
||||
play({.index = defaultAnimationID, .mode = SET});
|
||||
play({.index = defaultAnimationID, .mode = SET});
|
||||
pending_animation_apply();
|
||||
|
||||
rotationOverrideID = util::vector::push_index(
|
||||
|
||||
+5
-5
@@ -134,6 +134,7 @@ namespace game
|
||||
Mode mode{PLAY};
|
||||
glm::vec2 position{};
|
||||
float time{};
|
||||
bool isShowLayers{};
|
||||
bool isShowNulls{};
|
||||
bool isVisible{true};
|
||||
int animationIndex{-1};
|
||||
@@ -156,9 +157,10 @@ namespace game
|
||||
int digestionTimer{};
|
||||
bool isDigesting{};
|
||||
bool isJustDigested{};
|
||||
float eatSpeed{1.0f};
|
||||
float totalCaloriesConsumed{};
|
||||
int totalFoodItemsEaten{};
|
||||
int totalItemsSpawned{};
|
||||
int totalItemsConsumed{};
|
||||
float totalPlaytimeSeconds{};
|
||||
int talkOverrideID{};
|
||||
float talkTimer{};
|
||||
bool isTalking{};
|
||||
@@ -185,7 +187,6 @@ namespace game
|
||||
bool isHeld{};
|
||||
int schemaID{};
|
||||
int rotationOverrideID{};
|
||||
int durability{};
|
||||
glm::vec2 velocity{};
|
||||
float angularVelocity{};
|
||||
float phase{};
|
||||
@@ -200,8 +201,7 @@ namespace game
|
||||
Entity& operator=(Entity&&) noexcept;
|
||||
Entity(resource::xml::Anm2, glm::vec2 position = {}, Mode = PLAY, float time = 0.0f, int animationIndex = -1);
|
||||
Entity(resource::xml::Character&, glm::ivec2);
|
||||
Entity(resource::xml::Anm2, glm::ivec2 position, int id, int durability = 0, int animationIndex = -1,
|
||||
glm::vec2 velocity = {}, float rotation = 0.0f);
|
||||
Entity(resource::xml::Anm2, glm::ivec2 position, int id, glm::vec2 velocity = {}, float rotation = 0.0f);
|
||||
|
||||
bool is_playing(const std::string& name = {});
|
||||
glm::vec4 null_frame_rect(int = -1, int animationIndex = -1, float time = -1.0f);
|
||||
|
||||
+107
-39
@@ -6,6 +6,7 @@
|
||||
#include "../log.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
@@ -17,6 +18,11 @@ namespace game::resource::xml
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::string_view SOURCE_DOCUMENT_TAG = "SourceDocument";
|
||||
constexpr auto ANM2_COLOR_MAX = 255.0f;
|
||||
constexpr auto ANM2_OVERLAY_DRAW_ORDER_BELOW = 0;
|
||||
constexpr auto ANM2_OVERLAY_DRAW_ORDER_ABOVE = 1;
|
||||
|
||||
XMLError document_load(const physfs::Path& path, XMLDocument& document)
|
||||
{
|
||||
if (!path.is_valid())
|
||||
@@ -51,10 +57,21 @@ namespace game::resource::xml
|
||||
{
|
||||
int temp{};
|
||||
auto result = element->QueryIntAttribute(attribute, &temp);
|
||||
if (result == XML_SUCCESS && value) *value = (temp / 255.0f);
|
||||
if (result == XML_SUCCESS && value) *value = (temp / ANM2_COLOR_MAX);
|
||||
return result;
|
||||
}
|
||||
|
||||
XMLElement* source_document_get(XMLElement* rootElement)
|
||||
{
|
||||
if (!rootElement) return nullptr;
|
||||
if (rootElement->Name() && SOURCE_DOCUMENT_TAG == rootElement->Name()) return rootElement;
|
||||
|
||||
for (auto child = rootElement->FirstChildElement(); child; child = child->NextSiblingElement())
|
||||
if (child->Name() && SOURCE_DOCUMENT_TAG == child->Name()) return child;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Anm2::Frame::Interpolation interpolation_get(XMLElement* element)
|
||||
{
|
||||
if (!element) return Anm2::Frame::INTERPOLATION_NONE;
|
||||
@@ -79,6 +96,10 @@ namespace game::resource::xml
|
||||
query_string_attribute(element, "DefaultAnimation", &parsed.defaultAnimation);
|
||||
query_string_attribute(element, "Name", &parsed.name);
|
||||
query_string_attribute(element, "Path", &parsed.path);
|
||||
query_string_attribute(element, "Vertex", &parsed.vertex);
|
||||
query_string_attribute(element, "Fragment", &parsed.fragment);
|
||||
query_string_attribute(element, "Binding", &parsed.binding);
|
||||
query_string_attribute(element, "Value", &parsed.value);
|
||||
|
||||
element->QueryIntAttribute("Id", &parsed.id);
|
||||
element->QueryIntAttribute("LayerId", &parsed.layerID);
|
||||
@@ -87,6 +108,8 @@ namespace game::resource::xml
|
||||
element->QueryIntAttribute("EventId", &parsed.eventID);
|
||||
element->QueryIntAttribute("RegionId", &parsed.regionID);
|
||||
element->QueryIntAttribute("GroupId", &parsed.groupID);
|
||||
element->QueryIntAttribute("SoundId", &parsed.soundID);
|
||||
element->QueryIntAttribute("Index", &parsed.index);
|
||||
element->QueryIntAttribute("AtFrame", &parsed.atFrame);
|
||||
element->QueryIntAttribute("FrameNum", &parsed.frameNum);
|
||||
element->QueryIntAttribute("Fps", &parsed.fps);
|
||||
@@ -97,6 +120,8 @@ namespace game::resource::xml
|
||||
element->QueryFloatAttribute("YCrop", &parsed.crop.y);
|
||||
element->QueryFloatAttribute("XPosition", &parsed.position.x);
|
||||
element->QueryFloatAttribute("YPosition", &parsed.position.y);
|
||||
element->QueryFloatAttribute("XOffset", &parsed.position.x);
|
||||
element->QueryFloatAttribute("YOffset", &parsed.position.y);
|
||||
element->QueryFloatAttribute("XPivot", &parsed.pivot.x);
|
||||
element->QueryFloatAttribute("YPivot", &parsed.pivot.y);
|
||||
element->QueryFloatAttribute("Width", &parsed.size.x);
|
||||
@@ -112,11 +137,18 @@ namespace game::resource::xml
|
||||
query_color_attribute(element, "RedOffset", &parsed.colorOffset.r);
|
||||
query_color_attribute(element, "GreenOffset", &parsed.colorOffset.g);
|
||||
query_color_attribute(element, "BlueOffset", &parsed.colorOffset.b);
|
||||
query_color_attribute(element, "Alpha", &parsed.tint.a);
|
||||
|
||||
element->QueryBoolAttribute("Visible", &parsed.isVisible);
|
||||
element->QueryBoolAttribute("ShowRect", &parsed.isShowRect);
|
||||
element->QueryBoolAttribute("Loop", &parsed.isLoop);
|
||||
element->QueryBoolAttribute("Expanded", &parsed.isExpanded);
|
||||
element->QueryBoolAttribute("IsExpanded", &parsed.isExpanded);
|
||||
element->QueryBoolAttribute("Enabled", &parsed.isEnabled);
|
||||
|
||||
if (auto drawOrder = element->Attribute("DrawOrder"))
|
||||
parsed.index = std::strcmp(drawOrder, "Below") == 0 ? ANM2_OVERLAY_DRAW_ORDER_BELOW
|
||||
: ANM2_OVERLAY_DRAW_ORDER_ABOVE;
|
||||
}
|
||||
|
||||
Anm2::Element element_parse(XMLElement* element)
|
||||
@@ -139,32 +171,12 @@ namespace game::resource::xml
|
||||
|
||||
Anm2::Element::Type Anm2::Element::type_get(std::string_view tag)
|
||||
{
|
||||
static constexpr std::array<std::pair<Type, std::string_view>, TYPE_COUNT - 1> TAGS{{
|
||||
{ACTOR, "AnimatedActor"},
|
||||
{INFO, "Info"},
|
||||
{CONTENT, "Content"},
|
||||
{SPRITESHEETS, "Spritesheets"},
|
||||
{SPRITESHEET, "Spritesheet"},
|
||||
{REGION, "Region"},
|
||||
{LAYERS, "Layers"},
|
||||
{LAYER_ELEMENT, "Layer"},
|
||||
{NULLS, "Nulls"},
|
||||
{NULL_ELEMENT, "Null"},
|
||||
{EVENTS, "Events"},
|
||||
{EVENT, "Event"},
|
||||
{SOUNDS, "Sounds"},
|
||||
{SOUND_ELEMENT, "Sound"},
|
||||
{ANIMATIONS, "Animations"},
|
||||
{ANIMATION, "Animation"},
|
||||
{ROOT_ANIMATION, "RootAnimation"},
|
||||
{LAYER_ANIMATIONS, "LayerAnimations"},
|
||||
{LAYER_ANIMATION, "LayerAnimation"},
|
||||
{NULL_ANIMATIONS, "NullAnimations"},
|
||||
{NULL_ANIMATION, "NullAnimation"},
|
||||
{GROUP, "Group"},
|
||||
{TRIGGERS, "Triggers"},
|
||||
{TRIGGER_ELEMENT, "Trigger"},
|
||||
{FRAME, "Frame"},
|
||||
if (tag == SOURCE_DOCUMENT_TAG) return ACTOR;
|
||||
|
||||
static constexpr std::array<std::pair<Type, std::string_view>, TYPE_COUNT> TAGS{{
|
||||
#define X(symbol, elementTag) {symbol, elementTag},
|
||||
GAME_ANM2_ELEMENT_TYPES(X)
|
||||
#undef X
|
||||
}};
|
||||
|
||||
for (auto [type, typeTag] : TAGS)
|
||||
@@ -215,8 +227,21 @@ namespace game::resource::xml
|
||||
|
||||
auto element = document.RootElement();
|
||||
if (!element) return;
|
||||
if (auto sourceElement = source_document_get(element)) element = sourceElement;
|
||||
root = element_parse(element);
|
||||
|
||||
auto tagged_children_parse = [](auto&& self, XMLElement* parent, const char* tag, auto&& callback) -> void
|
||||
{
|
||||
for (auto child = parent->FirstChildElement(); child; child = child->NextSiblingElement())
|
||||
{
|
||||
auto name = child->Name();
|
||||
if (name && std::strcmp(name, tag) == 0)
|
||||
callback(child);
|
||||
else if (name && std::strcmp(name, "Group") == 0)
|
||||
self(self, child, tag, callback);
|
||||
}
|
||||
};
|
||||
|
||||
auto parse_frame = [&](XMLElement* element, Type type, int spritesheetID = -1)
|
||||
{
|
||||
Frame frame{};
|
||||
@@ -288,11 +313,12 @@ namespace game::resource::xml
|
||||
return frame;
|
||||
};
|
||||
|
||||
auto parse_item = [&](XMLElement* element, Type type, int& id)
|
||||
auto parse_item = [&](XMLElement* element, Type type, int& id, int groupID = -1)
|
||||
{
|
||||
Item item{};
|
||||
if (!element) return item;
|
||||
|
||||
item.groupID = groupID;
|
||||
if (type == LAYER) element->QueryIntAttribute("LayerId", &id);
|
||||
if (type == NULL_) element->QueryIntAttribute("NullId", &id);
|
||||
|
||||
@@ -323,23 +349,41 @@ namespace game::resource::xml
|
||||
if (auto rootAnimationElement = element->FirstChildElement("RootAnimation"))
|
||||
animation.rootAnimation = parse_item(rootAnimationElement, ROOT, id);
|
||||
|
||||
auto track_elements_parse = [](auto&& self, XMLElement* parent, const char* tag, auto&& callback) -> void
|
||||
auto track_children_parse = [&](auto&& self, XMLElement* parent, const char* tag, int parentGroupID,
|
||||
auto&& callback) -> void
|
||||
{
|
||||
for (auto child = parent->FirstChildElement(); child; child = child->NextSiblingElement())
|
||||
{
|
||||
auto name = child->Name();
|
||||
if (name && std::strcmp(name, tag) == 0)
|
||||
callback(child);
|
||||
{
|
||||
callback(child, parentGroupID);
|
||||
}
|
||||
else if (name && std::strcmp(name, "Group") == 0)
|
||||
self(self, child, tag, callback);
|
||||
{
|
||||
Group group{};
|
||||
int groupRootID{-1};
|
||||
group.parent = parentGroupID;
|
||||
child->QueryBoolAttribute("Visible", &group.isVisible);
|
||||
if (auto rootAnimationElement = child->FirstChildElement("RootAnimation"))
|
||||
group.rootAnimation = parse_item(rootAnimationElement, GROUP, groupRootID);
|
||||
auto groupID = (int)animation.groups.size();
|
||||
animation.groups.emplace_back(std::move(group));
|
||||
self(self, child, tag, groupID, callback);
|
||||
}
|
||||
else if (name && (std::strcmp(name, "LayerAnimationGroups") == 0 ||
|
||||
std::strcmp(name, "NullAnimationGroups") == 0))
|
||||
self(self, child, tag, parentGroupID, callback);
|
||||
}
|
||||
};
|
||||
|
||||
if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations"))
|
||||
{
|
||||
track_elements_parse(track_elements_parse, layerAnimationsElement, "LayerAnimation", [&](XMLElement* child)
|
||||
track_children_parse(track_children_parse, layerAnimationsElement, "LayerAnimation", -1,
|
||||
[&](XMLElement* child, int groupID)
|
||||
{
|
||||
auto layerAnimation = parse_item(child, LAYER, id);
|
||||
id = -1;
|
||||
auto layerAnimation = parse_item(child, LAYER, id, groupID);
|
||||
animation.layerOrder.push_back(id);
|
||||
animation.layerAnimations.emplace(id, std::move(layerAnimation));
|
||||
});
|
||||
@@ -347,9 +391,12 @@ namespace game::resource::xml
|
||||
|
||||
if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
|
||||
{
|
||||
track_elements_parse(track_elements_parse, nullAnimationsElement, "NullAnimation", [&](XMLElement* child)
|
||||
track_children_parse(track_children_parse, nullAnimationsElement, "NullAnimation", -1,
|
||||
[&](XMLElement* child, int groupID)
|
||||
{
|
||||
auto nullAnimation = parse_item(child, NULL_, id);
|
||||
id = -1;
|
||||
auto nullAnimation = parse_item(child, NULL_, id, groupID);
|
||||
animation.nullOrder.push_back(id);
|
||||
animation.nullAnimations.emplace(id, std::move(nullAnimation));
|
||||
});
|
||||
}
|
||||
@@ -480,9 +527,11 @@ namespace game::resource::xml
|
||||
{
|
||||
query_string_attribute(animationsElement, "DefaultAnimation", &defaultAnimation);
|
||||
|
||||
for (auto child = animationsElement->FirstChildElement("Animation"); child;
|
||||
child = child->NextSiblingElement("Animation"))
|
||||
bool isDefaultAnimationParsed{};
|
||||
tagged_children_parse(tagged_children_parse, animationsElement, "Animation", [&](XMLElement* child)
|
||||
{
|
||||
if (isDefaultAnimationParsed) return;
|
||||
|
||||
if ((this->flags & DEFAULT_ANIMATION_ONLY) != 0)
|
||||
{
|
||||
std::string name{};
|
||||
@@ -490,12 +539,23 @@ namespace game::resource::xml
|
||||
if (name == defaultAnimation)
|
||||
{
|
||||
animations.emplace_back(parse_animation(child));
|
||||
break;
|
||||
isDefaultAnimationParsed = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
animations.emplace_back(parse_animation(child));
|
||||
}
|
||||
});
|
||||
|
||||
if ((this->flags & DEFAULT_ANIMATION_ONLY) != 0 && animations.empty())
|
||||
tagged_children_parse(tagged_children_parse, animationsElement, "Animation", [&](XMLElement* child)
|
||||
{
|
||||
if (!isDefaultAnimationParsed)
|
||||
{
|
||||
animations.emplace_back(parse_animation(child));
|
||||
isDefaultAnimationParsed = true;
|
||||
}
|
||||
});
|
||||
|
||||
for (int i = 0; i < (int)animations.size(); i++)
|
||||
{
|
||||
@@ -552,6 +612,14 @@ namespace game::resource::xml
|
||||
|
||||
bool Anm2::is_valid() const { return isValid; }
|
||||
|
||||
bool Anm2::is_rect_valid(glm::vec4 rect)
|
||||
{
|
||||
static constexpr auto ZERO_FLOAT = 0.0f;
|
||||
|
||||
return std::isfinite(rect.x) && std::isfinite(rect.y) && std::isfinite(rect.z) && std::isfinite(rect.w) &&
|
||||
rect.z > ZERO_FLOAT && rect.w > ZERO_FLOAT;
|
||||
}
|
||||
|
||||
int Anm2::item_length(const Item* item)
|
||||
{
|
||||
if (!item) return -1;
|
||||
|
||||
+56
-26
@@ -18,6 +18,40 @@
|
||||
|
||||
#include "../util/physfs.hpp"
|
||||
|
||||
#define GAME_ANM2_ELEMENT_TYPES(X) \
|
||||
X(UNKNOWN, "") \
|
||||
X(ACTOR, "AnimatedActor") \
|
||||
X(INFO, "Info") \
|
||||
X(CONTENT, "Content") \
|
||||
X(SPRITESHEETS, "Spritesheets") \
|
||||
X(SPRITESHEET, "Spritesheet") \
|
||||
X(OVERLAY, "Overlay") \
|
||||
X(SHADER, "Shader") \
|
||||
X(UNIFORM, "Uniform") \
|
||||
X(COMPONENT, "Component") \
|
||||
X(REGION, "Region") \
|
||||
X(LAYERS, "Layers") \
|
||||
X(LAYER_ELEMENT, "Layer") \
|
||||
X(NULLS, "Nulls") \
|
||||
X(NULL_ELEMENT, "Null") \
|
||||
X(EVENTS, "Events") \
|
||||
X(EVENT, "Event") \
|
||||
X(SOUNDS, "Sounds") \
|
||||
X(SOUND_ELEMENT, "Sound") \
|
||||
X(ANIMATIONS, "Animations") \
|
||||
X(ANIMATION, "Animation") \
|
||||
X(ROOT_ANIMATION, "RootAnimation") \
|
||||
X(LAYER_ANIMATIONS, "LayerAnimations") \
|
||||
X(LAYER_ANIMATION_GROUPS, "LayerAnimationGroups") \
|
||||
X(LAYER_ANIMATION, "LayerAnimation") \
|
||||
X(NULL_ANIMATIONS, "NullAnimations") \
|
||||
X(NULL_ANIMATION_GROUPS, "NullAnimationGroups") \
|
||||
X(NULL_ANIMATION, "NullAnimation") \
|
||||
X(GROUP, "Group") \
|
||||
X(TRIGGERS, "Triggers") \
|
||||
X(TRIGGER_ELEMENT, "Trigger") \
|
||||
X(FRAME, "Frame")
|
||||
|
||||
namespace game::resource::xml
|
||||
{
|
||||
class Anm2
|
||||
@@ -27,6 +61,7 @@ namespace game::resource::xml
|
||||
{
|
||||
NONE,
|
||||
ROOT,
|
||||
GROUP,
|
||||
LAYER,
|
||||
NULL_,
|
||||
TRIGGER
|
||||
@@ -45,32 +80,9 @@ namespace game::resource::xml
|
||||
{
|
||||
enum Type
|
||||
{
|
||||
UNKNOWN,
|
||||
ACTOR,
|
||||
INFO,
|
||||
CONTENT,
|
||||
SPRITESHEETS,
|
||||
SPRITESHEET,
|
||||
REGION,
|
||||
LAYERS,
|
||||
LAYER_ELEMENT,
|
||||
NULLS,
|
||||
NULL_ELEMENT,
|
||||
EVENTS,
|
||||
EVENT,
|
||||
SOUNDS,
|
||||
SOUND_ELEMENT,
|
||||
ANIMATIONS,
|
||||
ANIMATION,
|
||||
ROOT_ANIMATION,
|
||||
LAYER_ANIMATIONS,
|
||||
LAYER_ANIMATION,
|
||||
NULL_ANIMATIONS,
|
||||
NULL_ANIMATION,
|
||||
GROUP,
|
||||
TRIGGERS,
|
||||
TRIGGER_ELEMENT,
|
||||
FRAME,
|
||||
#define X(symbol, tag) symbol,
|
||||
GAME_ANM2_ELEMENT_TYPES(X)
|
||||
#undef X
|
||||
TYPE_COUNT
|
||||
};
|
||||
|
||||
@@ -82,6 +94,10 @@ namespace game::resource::xml
|
||||
std::string defaultAnimation{};
|
||||
std::string name{};
|
||||
std::string path{};
|
||||
std::string vertex{};
|
||||
std::string fragment{};
|
||||
std::string binding{};
|
||||
std::string value{};
|
||||
|
||||
int id{-1};
|
||||
int layerID{-1};
|
||||
@@ -90,6 +106,8 @@ namespace game::resource::xml
|
||||
int eventID{-1};
|
||||
int regionID{-1};
|
||||
int groupID{-1};
|
||||
int soundID{-1};
|
||||
int index{-1};
|
||||
int atFrame{-1};
|
||||
int frameNum{};
|
||||
int fps{30};
|
||||
@@ -109,6 +127,7 @@ namespace game::resource::xml
|
||||
bool isShowRect{};
|
||||
bool isLoop{};
|
||||
bool isExpanded{true};
|
||||
bool isEnabled{true};
|
||||
|
||||
Element* child_get(Type, std::size_t index = 0);
|
||||
const Element* child_get(Type, std::size_t index = 0) const;
|
||||
@@ -264,9 +283,17 @@ namespace game::resource::xml
|
||||
struct Item
|
||||
{
|
||||
std::vector<Frame> frames{};
|
||||
int groupID{-1};
|
||||
bool isVisible{};
|
||||
};
|
||||
|
||||
struct Group
|
||||
{
|
||||
Item rootAnimation{};
|
||||
int parent{-1};
|
||||
bool isVisible{true};
|
||||
};
|
||||
|
||||
struct Animation
|
||||
{
|
||||
std::string name{"New Animation"};
|
||||
@@ -274,9 +301,11 @@ namespace game::resource::xml
|
||||
bool isLoop{};
|
||||
|
||||
Item rootAnimation{};
|
||||
std::vector<Group> groups{};
|
||||
std::unordered_map<int, Item> layerAnimations{};
|
||||
std::vector<int> layerOrder{};
|
||||
std::map<int, Item> nullAnimations{};
|
||||
std::vector<int> nullOrder{};
|
||||
Item triggers{};
|
||||
};
|
||||
|
||||
@@ -311,6 +340,7 @@ namespace game::resource::xml
|
||||
Anm2(const util::physfs::Path&, Flags = 0);
|
||||
|
||||
bool is_valid() const;
|
||||
static bool is_rect_valid(glm::vec4);
|
||||
static int item_length(const Item*);
|
||||
static float interpolation_apply(Frame::Interpolation, float);
|
||||
static Frame frame_generate(Item&, float, Type = NONE, int id = -1,
|
||||
|
||||
@@ -166,14 +166,16 @@ namespace game::resource
|
||||
|
||||
void Audio::play(bool isLoop) const
|
||||
{
|
||||
if (!internal) return;
|
||||
auto audio = internal;
|
||||
if (!audio) return;
|
||||
|
||||
auto mixer = mixer_get();
|
||||
if (!mixer) return;
|
||||
|
||||
if (track && MIX_GetTrackMixer(track) != mixer)
|
||||
if (!isLoop)
|
||||
{
|
||||
MIX_DestroyTrack(track);
|
||||
track = nullptr;
|
||||
MIX_PlayAudio(mixer, audio.get());
|
||||
return;
|
||||
}
|
||||
|
||||
if (!track)
|
||||
@@ -182,7 +184,7 @@ namespace game::resource
|
||||
if (!track) return;
|
||||
}
|
||||
|
||||
MIX_SetTrackAudio(track, internal.get());
|
||||
if (!MIX_SetTrackAudio(track, audio.get())) return;
|
||||
|
||||
SDL_PropertiesID options = 0;
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace game::resource
|
||||
static constexpr auto HEADER_1 = 40;
|
||||
static constexpr auto HEADER_2 = 30;
|
||||
static constexpr auto HEADER_3 = 24;
|
||||
static constexpr auto HUGE = 80;
|
||||
|
||||
Font() = default;
|
||||
Font(const std::filesystem::path&, float = NORMAL);
|
||||
|
||||
+132
-70
@@ -17,15 +17,16 @@ namespace game::resource::xml
|
||||
std::string* Schema::AnimationEntryCollection::get()
|
||||
{
|
||||
if (empty()) return nullptr;
|
||||
return &at(util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; })).animation;
|
||||
auto index = util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; });
|
||||
return index < 0 || index >= (int)size() ? nullptr : &at(index).animation;
|
||||
}
|
||||
|
||||
const Audio* Schema::SoundEntryCollection::get() const
|
||||
{
|
||||
if (empty()) return nullptr;
|
||||
auto index =
|
||||
util::vector::random_index_weighted(*this, [](const auto* entry) { return entry ? entry->weight : 0.0f; });
|
||||
return index < 0 || !at(index) ? nullptr : &at(index)->sound;
|
||||
util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; });
|
||||
return index < 0 || index >= (int)size() ? nullptr : &at(index).sound;
|
||||
}
|
||||
|
||||
void Schema::SoundEntryCollection::play() const
|
||||
@@ -34,8 +35,8 @@ namespace game::resource::xml
|
||||
auto audio = get();
|
||||
if (!audio) return;
|
||||
|
||||
for (auto* entry : *this)
|
||||
if (entry && &entry->sound != audio) entry->sound.stop();
|
||||
for (auto& entry : *this)
|
||||
if (&entry.sound != audio) entry.sound.stop();
|
||||
|
||||
audio->play();
|
||||
}
|
||||
@@ -67,6 +68,11 @@ namespace game::resource::xml
|
||||
return result;
|
||||
}
|
||||
|
||||
bool element_index_is_valid(const Schema& schema, int index)
|
||||
{
|
||||
return index >= 0 && index < (int)schema.elements.size();
|
||||
}
|
||||
|
||||
XMLError query_color_attribute(XMLElement* element, const char* attribute, float* value)
|
||||
{
|
||||
int temp{};
|
||||
@@ -137,8 +143,10 @@ namespace game::resource::xml
|
||||
X(Color) \
|
||||
X(Delay) \
|
||||
X(Oscillation) \
|
||||
X(Scene) \
|
||||
X(Screenshake) \
|
||||
X(Shake) \
|
||||
X(Silent) \
|
||||
X(Small) \
|
||||
X(Sound)
|
||||
|
||||
@@ -231,6 +239,7 @@ namespace game::resource::xml
|
||||
Schema::Anm2Entry baseAnm2{};
|
||||
int itemSpritesheetID{};
|
||||
bool isDialogue{};
|
||||
bool isSave{};
|
||||
};
|
||||
|
||||
std::string resource_path_get(const Context& context, const std::string& rootPath, const std::string& path)
|
||||
@@ -380,7 +389,13 @@ namespace game::resource::xml
|
||||
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_STRING_TARGET
|
||||
#undef GAME_SCHEMA_QUERY_ATTRIBUTE_STRING
|
||||
|
||||
if (element.type == Schema::Element::POOL_ENTRY) query_string(xml, "Entry", element.idString);
|
||||
if (element.type == Schema::Element::POOL_ENTRY)
|
||||
{
|
||||
query_string(xml, "Label", element.idString);
|
||||
if (element.idString.empty()) query_string(xml, "Entry", element.idString);
|
||||
}
|
||||
else if (element.type == Schema::Element::HOOK)
|
||||
query_string(xml, "Entry", element.idString);
|
||||
|
||||
if (!element.textureRootPath.empty()) resourceContext.textureRootPath = element.textureRootPath;
|
||||
if (!element.anm2RootPath.empty())
|
||||
@@ -520,10 +535,20 @@ namespace game::resource::xml
|
||||
if (symbol == "typeString" && element.type == Schema::Element::EFFECT)
|
||||
return true;
|
||||
if (symbol == "idString" && element.type == Schema::Element::POOL_ENTRY)
|
||||
{
|
||||
set_string("Label", element.idString);
|
||||
return true;
|
||||
}
|
||||
if (symbol == "idString" && element.type == Schema::Element::HOOK)
|
||||
{
|
||||
set_string("Entry", element.idString);
|
||||
return true;
|
||||
}
|
||||
if (symbol == "id" && context.isSave && element.type == Schema::Element::ITEM)
|
||||
{
|
||||
xml->SetAttribute("ID", element.id);
|
||||
return true;
|
||||
}
|
||||
#define GAME_SCHEMA_WRITE_ATTRIBUTE_STRING(KEY, KEY_2, KEY_3, MEMBER) \
|
||||
set_string(KEY, resource_string_get(#MEMBER, element.MEMBER));
|
||||
#define GAME_SCHEMA_WRITE_ATTRIBUTE_STRING_TARGET(KEY, KEY_2, KEY_3, MEMBER) \
|
||||
@@ -603,10 +628,14 @@ namespace game::resource::xml
|
||||
if (!source.anm2RootPath.empty()) context.anm2RootPath = source.anm2RootPath;
|
||||
if (!source.soundRootPath.empty()) context.soundRootPath = source.soundRootPath;
|
||||
if (!source.fontRootPath.empty()) context.fontRootPath = source.fontRootPath;
|
||||
if (source.type == Schema::Element::SAVE) context.isSave = true;
|
||||
|
||||
element_attributes_write(document, xml, source, context);
|
||||
for (auto child : source.children)
|
||||
{
|
||||
if (!element_index_is_valid(schema, child)) continue;
|
||||
xml->InsertEndChild(element_write(document, schema, child, context));
|
||||
}
|
||||
return xml;
|
||||
}
|
||||
|
||||
@@ -619,14 +648,15 @@ namespace game::resource::xml
|
||||
void sound_entry_add(Schema::Element& element, Schema::SoundEntryCollection& collection)
|
||||
{
|
||||
if (!element.soundEntry.is_valid()) return;
|
||||
element.soundEntry.weight = element.weight == 0.0f ? 1.0f : element.weight;
|
||||
collection.push_back(&element.soundEntry);
|
||||
auto entry = element.soundEntry;
|
||||
entry.weight = element.weight == 0.0f ? 1.0f : element.weight;
|
||||
collection.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
void sound_entries_add(const Schema::SoundEntryCollection& source, Schema::SoundEntryCollection& collection)
|
||||
{
|
||||
for (auto* entry : source)
|
||||
if (entry) collection.push_back(entry);
|
||||
for (auto& entry : source)
|
||||
if (entry.is_valid()) collection.push_back(entry);
|
||||
}
|
||||
|
||||
bool element_attribute_has(const Schema::Element& element, std::string_view attribute)
|
||||
@@ -704,10 +734,16 @@ namespace game::resource::xml
|
||||
X(RELEASE, soundRelease) \
|
||||
X(SELECT, soundSelect) \
|
||||
X(SUMMON, soundSummon) \
|
||||
X(THROW, soundThrow) \
|
||||
X(RETURN_, soundReturn) \
|
||||
X(UPGRADE, soundUpgrade) \
|
||||
X(UPGRADE_FAIL, soundUpgradeFail)
|
||||
X(THROW, soundThrow)
|
||||
|
||||
#define GAME_SCHEMA_ACTION_ANIMATIONS(X) \
|
||||
X(CLICK, animationClick) \
|
||||
X(DISPOSE, animationDispose) \
|
||||
X(GRAB, animationGrab) \
|
||||
X(HOVER, animationHover) \
|
||||
X(IDLE, animationIdle) \
|
||||
X(PAN, animationPan) \
|
||||
X(ZOOM, animationZoom)
|
||||
|
||||
void cache_refresh(Schema& schema)
|
||||
{
|
||||
@@ -725,8 +761,6 @@ namespace game::resource::xml
|
||||
schema.baseAnm2 = {};
|
||||
schema.font = {};
|
||||
schema.iconsFont = {};
|
||||
schema.durability = 2;
|
||||
schema.quantityMax = 99;
|
||||
schema.rounding = 0.0f;
|
||||
|
||||
auto* root = schema.root();
|
||||
@@ -755,7 +789,7 @@ namespace game::resource::xml
|
||||
root->animationIdle = {};
|
||||
root->animationClick = {};
|
||||
root->animationPan = {};
|
||||
root->animationReturn = {};
|
||||
root->animationDispose = {};
|
||||
root->animationZoom = {};
|
||||
root->soundBlip = {};
|
||||
root->soundBounce = {};
|
||||
@@ -772,20 +806,17 @@ namespace game::resource::xml
|
||||
root->soundLevelUpReady = {};
|
||||
root->soundOpen = {};
|
||||
root->soundRelease = {};
|
||||
root->soundReturn = {};
|
||||
root->soundSelect = {};
|
||||
root->soundSummon = {};
|
||||
root->soundThrow = {};
|
||||
root->soundUpgrade = {};
|
||||
root->soundUpgradeFail = {};
|
||||
root->animationChew = {};
|
||||
|
||||
for (auto& element : schema.elements)
|
||||
{
|
||||
element.soundEntries = {};
|
||||
for (auto childID : element.children)
|
||||
{
|
||||
auto& child = schema.elements.at(childID);
|
||||
if (!element_index_is_valid(schema, childID)) continue;
|
||||
auto& child = schema.elements[childID];
|
||||
if (child.type == Schema::Element::SOUND) sound_entry_add(child, element.soundEntries);
|
||||
}
|
||||
}
|
||||
@@ -810,10 +841,8 @@ namespace game::resource::xml
|
||||
animation_entry_add(*element, root->animationPan);
|
||||
if (auto* element = schema.child_get(*animations, Schema::Element::ZOOM))
|
||||
animation_entry_add(*element, root->animationZoom);
|
||||
if (auto* element = schema.child_get(*animations, Schema::Element::RETURN_))
|
||||
animation_entry_add(*element, root->animationReturn);
|
||||
if (auto* element = schema.child_get(*animations, Schema::Element::CHEW))
|
||||
root->animationChew = element->animation;
|
||||
if (auto* element = schema.child_get(*animations, Schema::Element::DISPOSE))
|
||||
animation_entry_add(*element, root->animationDispose);
|
||||
}
|
||||
|
||||
if (auto* sounds = schema.child_get(*root, Schema::Element::SOUNDS))
|
||||
@@ -848,21 +877,21 @@ namespace game::resource::xml
|
||||
sound_entry_add(*element, root->soundOpen);
|
||||
for (auto* element : schema.children_get(*sounds, Schema::Element::RELEASE))
|
||||
sound_entry_add(*element, root->soundRelease);
|
||||
for (auto* element : schema.children_get(*sounds, Schema::Element::RETURN_))
|
||||
sound_entry_add(*element, root->soundReturn);
|
||||
for (auto* element : schema.children_get(*sounds, Schema::Element::SELECT))
|
||||
sound_entry_add(*element, root->soundSelect);
|
||||
for (auto* element : schema.children_get(*sounds, Schema::Element::SUMMON))
|
||||
sound_entry_add(*element, root->soundSummon);
|
||||
for (auto* element : schema.children_get(*sounds, Schema::Element::THROW))
|
||||
sound_entry_add(*element, root->soundThrow);
|
||||
for (auto* element : schema.children_get(*sounds, Schema::Element::UPGRADE))
|
||||
sound_entry_add(*element, root->soundUpgrade);
|
||||
for (auto* element : schema.children_get(*sounds, Schema::Element::UPGRADE_FAIL))
|
||||
sound_entry_add(*element, root->soundUpgradeFail);
|
||||
}
|
||||
if (auto* actions = schema.child_get(*root, Schema::Element::ACTIONS))
|
||||
{
|
||||
#define GAME_SCHEMA_ACTION_ANIMATION_CACHE(TYPE, MEMBER) \
|
||||
if (auto* action = schema.child_get(*actions, Schema::Element::TYPE)) \
|
||||
animation_entry_add(*action, root->MEMBER);
|
||||
GAME_SCHEMA_ACTION_ANIMATIONS(GAME_SCHEMA_ACTION_ANIMATION_CACHE)
|
||||
#undef GAME_SCHEMA_ACTION_ANIMATION_CACHE
|
||||
|
||||
#define GAME_SCHEMA_ACTION_SOUND_CACHE(TYPE, MEMBER) \
|
||||
for (auto* action : schema.children_get(*actions, Schema::Element::TYPE)) \
|
||||
for (auto* element : schema.children_get(*action, Schema::Element::SOUND)) \
|
||||
@@ -871,6 +900,7 @@ namespace game::resource::xml
|
||||
#undef GAME_SCHEMA_ACTION_SOUND_CACHE
|
||||
}
|
||||
#undef GAME_SCHEMA_ACTION_SOUNDS
|
||||
#undef GAME_SCHEMA_ACTION_ANIMATIONS
|
||||
if (auto* textPlayback = schema.child_get(*root, Schema::Element::TEXT_PLAYBACK))
|
||||
if (auto* blip = schema.child_get(*textPlayback, Schema::Element::BLIP))
|
||||
sound_entries_add(blip->soundEntries, root->soundBlip);
|
||||
@@ -879,37 +909,34 @@ namespace game::resource::xml
|
||||
|
||||
for (auto* element : schema.get_all(Schema::Element::AREA))
|
||||
schema.areas.push_back({.texture = element->textureEntry.texture,
|
||||
.anm2 = element->anm2Entry.anm2,
|
||||
.anm2 = element->anm2Entry.is_valid() ? element->anm2Entry.anm2 : schema.baseAnm2,
|
||||
.gravity = element->gravity == 0.0f ? 0.95f : element->gravity,
|
||||
.friction = element->friction == 0.0f ? 0.80f : element->friction,
|
||||
.airResistance = element->airResistance == 0.0f ? 0.975f : element->airResistance});
|
||||
if (root->type == Schema::Element::AREAS && schema.areas.empty()) schema.areas.emplace_back();
|
||||
if (root->type == Schema::Element::AREAS && schema.areas.empty())
|
||||
schema.areas.emplace_back(Schema::AreaEntry{.anm2 = schema.baseAnm2});
|
||||
|
||||
for (auto* element : schema.get_all(Schema::Element::CATEGORY))
|
||||
schema.categories.push_back({.name = element->name, .isEdible = element->isEdible});
|
||||
schema.categories.push_back({.name = element->name, .useMode = element->useMode});
|
||||
|
||||
for (auto* element : schema.get_all(Schema::Element::RARITY))
|
||||
{
|
||||
schema.rarityIDs.push_back((int)schema.rarities.size());
|
||||
schema.rarities.push_back({.name = element->name.empty() ? "???" : element->name,
|
||||
.weight = element->weight,
|
||||
.isHidden = element->isHidden,
|
||||
.isHiddenInCatalog = element->isHiddenInCatalog,
|
||||
.sound = element->soundEntry.sound});
|
||||
}
|
||||
|
||||
for (auto* element : schema.get_all(Schema::Element::FLAVOR))
|
||||
schema.flavors.push_back({.name = element->name.empty() ? "???" : element->name});
|
||||
|
||||
if (auto* itemsRoot = schema.get(Schema::Element::ITEMS))
|
||||
{
|
||||
if (itemsRoot->durability != 0) schema.durability = itemsRoot->durability;
|
||||
if (itemsRoot->quantityMax != 0) schema.quantityMax = itemsRoot->quantityMax;
|
||||
}
|
||||
|
||||
auto itemElements = schema.get_all(Schema::Element::ITEM);
|
||||
for (auto* element : itemElements)
|
||||
{
|
||||
if (element->parent < 0 || schema.elements[element->parent].type != Schema::Element::ITEMS) continue;
|
||||
if (!element_index_is_valid(schema, element->parent) ||
|
||||
schema.elements[element->parent].type != Schema::Element::ITEMS)
|
||||
continue;
|
||||
auto itemID = (int)schema.stringToIDMap.size();
|
||||
schema.stringToIDMap[element->name] = itemID;
|
||||
schema.idToStringMap[itemID] = element->name;
|
||||
@@ -917,7 +944,9 @@ namespace game::resource::xml
|
||||
|
||||
for (auto* element : itemElements)
|
||||
{
|
||||
if (element->parent < 0 || schema.elements[element->parent].type != Schema::Element::ITEMS) continue;
|
||||
if (!element_index_is_valid(schema, element->parent) ||
|
||||
schema.elements[element->parent].type != Schema::Element::ITEMS)
|
||||
continue;
|
||||
Schema::ItemEntry item{};
|
||||
item.name = element->name.empty() ? "???" : element->name;
|
||||
item.description = element->description.empty() ? "???" : element->description;
|
||||
@@ -931,21 +960,10 @@ namespace game::resource::xml
|
||||
item.capacityBonus = element->capacityBonus;
|
||||
item.isDigestionBonus = element->digestionBonus != 0.0f;
|
||||
item.digestionBonus = element->digestionBonus;
|
||||
item.isEatSpeedBonus = element->eatSpeedBonus != 0.0f;
|
||||
item.eatSpeedBonus = element->eatSpeedBonus;
|
||||
item.isGravity = element->gravity != 0.0f;
|
||||
item.gravity = element->gravity;
|
||||
item.isDurability = element->durability != 0;
|
||||
item.durability = element->durability;
|
||||
item.isColor = element->color != glm::vec3{};
|
||||
item.color = element->color;
|
||||
if (!element->upgradeID.empty() && schema.stringToIDMap.contains(element->upgradeID))
|
||||
{
|
||||
item.isUpgradeID = true;
|
||||
item.upgradeID = schema.stringToIDMap[element->upgradeID];
|
||||
}
|
||||
item.isUpgradeCount = element->upgradeCount != 0;
|
||||
item.upgradeCount = element->upgradeCount;
|
||||
item.isToggleSpritesheet = element->isToggleSpritesheet;
|
||||
schema.pools[item.rarityID].push_back((int)schema.items.size());
|
||||
schema.anm2s.push_back(element->anm2Entry.anm2);
|
||||
@@ -1072,6 +1090,7 @@ namespace game::resource::xml
|
||||
root.measurementSystem = "Metric";
|
||||
root.volume = 50;
|
||||
root.isAudioMuted = false;
|
||||
root.isDeveloperConsoleEnabled = false;
|
||||
root.color = {0.120f, 0.515f, 0.115f};
|
||||
root.windowSize = {1600, 900};
|
||||
root.isUseCharacterColor = true;
|
||||
@@ -1108,7 +1127,10 @@ namespace game::resource::xml
|
||||
auto index = (int)elements.size();
|
||||
elements.emplace_back(std::move(element));
|
||||
elementsByType[type].push_back(index);
|
||||
if (parent >= 0) elements.at(parent).children.emplace_back(index);
|
||||
if (parent >= 0 && parent < index)
|
||||
elements[parent].children.emplace_back(index);
|
||||
else
|
||||
elements[index].parent = -1;
|
||||
isValid = true;
|
||||
return elements.back();
|
||||
}
|
||||
@@ -1139,7 +1161,8 @@ namespace game::resource::xml
|
||||
|
||||
for (auto& element : kept)
|
||||
{
|
||||
if (element.parent >= 0) element.parent = remap[element.parent];
|
||||
if (element.parent >= 0)
|
||||
element.parent = element.parent < (int)remap.size() ? remap[element.parent] : -1;
|
||||
|
||||
std::vector<int> children{};
|
||||
children.reserve(element.children.size());
|
||||
@@ -1192,11 +1215,11 @@ namespace game::resource::xml
|
||||
if (parent < 0 || parent >= (int)elements.size()) return false;
|
||||
if (index == parent || index == beforeSibling) return false;
|
||||
|
||||
for (auto ancestor = parent; ancestor >= 0; ancestor = elements[ancestor].parent)
|
||||
for (auto ancestor = parent; element_index_is_valid(*this, ancestor); ancestor = elements[ancestor].parent)
|
||||
if (ancestor == index) return false;
|
||||
|
||||
auto oldParent = elements[index].parent;
|
||||
if (oldParent >= 0)
|
||||
if (element_index_is_valid(*this, oldParent))
|
||||
{
|
||||
auto& oldSiblings = elements[oldParent].children;
|
||||
oldSiblings.erase(std::remove(oldSiblings.begin(), oldSiblings.end(), index), oldSiblings.end());
|
||||
@@ -1228,14 +1251,16 @@ namespace game::resource::xml
|
||||
{
|
||||
auto it = elementsByType.find(type);
|
||||
if (it == elementsByType.end() || index < 0 || index >= (int)it->second.size()) return nullptr;
|
||||
return &elements.at(it->second.at(index));
|
||||
auto elementIndex = it->second[index];
|
||||
return element_index_is_valid(*this, elementIndex) ? &elements[elementIndex] : nullptr;
|
||||
}
|
||||
|
||||
const Schema::Element* Schema::get(Element::Type type, int index) const
|
||||
{
|
||||
auto it = elementsByType.find(type);
|
||||
if (it == elementsByType.end() || index < 0 || index >= (int)it->second.size()) return nullptr;
|
||||
return &elements.at(it->second.at(index));
|
||||
auto elementIndex = it->second[index];
|
||||
return element_index_is_valid(*this, elementIndex) ? &elements[elementIndex] : nullptr;
|
||||
}
|
||||
|
||||
std::vector<Schema::Element*> Schema::get_all(Element::Type type)
|
||||
@@ -1245,7 +1270,7 @@ namespace game::resource::xml
|
||||
if (it == elementsByType.end()) return result;
|
||||
result.reserve(it->second.size());
|
||||
for (auto index : it->second)
|
||||
result.emplace_back(&elements.at(index));
|
||||
if (element_index_is_valid(*this, index)) result.emplace_back(&elements[index]);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1256,7 +1281,7 @@ namespace game::resource::xml
|
||||
if (it == elementsByType.end()) return result;
|
||||
result.reserve(it->second.size());
|
||||
for (auto index : it->second)
|
||||
result.emplace_back(&elements.at(index));
|
||||
if (element_index_is_valid(*this, index)) result.emplace_back(&elements[index]);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1264,7 +1289,8 @@ namespace game::resource::xml
|
||||
{
|
||||
for (auto child : element.children)
|
||||
{
|
||||
auto& candidate = elements.at(child);
|
||||
if (!element_index_is_valid(*this, child)) continue;
|
||||
auto& candidate = elements[child];
|
||||
if (candidate.type != type) continue;
|
||||
if (index-- == 0) return &candidate;
|
||||
}
|
||||
@@ -1275,7 +1301,8 @@ namespace game::resource::xml
|
||||
{
|
||||
for (auto child : element.children)
|
||||
{
|
||||
auto& candidate = elements.at(child);
|
||||
if (!element_index_is_valid(*this, child)) continue;
|
||||
auto& candidate = elements[child];
|
||||
if (candidate.type != type) continue;
|
||||
if (index-- == 0) return &candidate;
|
||||
}
|
||||
@@ -1286,7 +1313,10 @@ namespace game::resource::xml
|
||||
{
|
||||
std::vector<Element*> result{};
|
||||
for (auto child : element.children)
|
||||
if (auto& candidate = elements.at(child); candidate.type == type) result.emplace_back(&candidate);
|
||||
{
|
||||
if (!element_index_is_valid(*this, child)) continue;
|
||||
if (auto& candidate = elements[child]; candidate.type == type) result.emplace_back(&candidate);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1294,7 +1324,10 @@ namespace game::resource::xml
|
||||
{
|
||||
std::vector<const Element*> result{};
|
||||
for (auto child : element.children)
|
||||
if (auto& candidate = elements.at(child); candidate.type == type) result.emplace_back(&candidate);
|
||||
{
|
||||
if (!element_index_is_valid(*this, child)) continue;
|
||||
if (auto& candidate = elements[child]; candidate.type == type) result.emplace_back(&candidate);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1308,7 +1341,7 @@ namespace game::resource::xml
|
||||
Schema::Element* Schema::dialogue_entry_get(const std::string& id)
|
||||
{
|
||||
auto it = entryIDMap.find(id);
|
||||
return it == entryIDMap.end() ? nullptr : &elements.at(it->second);
|
||||
return it == entryIDMap.end() || !element_index_is_valid(*this, it->second) ? nullptr : &elements[it->second];
|
||||
}
|
||||
|
||||
Schema::Element* Schema::dialogue_entry_get(Element& reference)
|
||||
@@ -1330,8 +1363,9 @@ namespace game::resource::xml
|
||||
{
|
||||
auto poolIt = poolMap.find(poolID);
|
||||
if (poolIt == poolMap.end()) return nullptr;
|
||||
if (!element_index_is_valid(*this, poolIt->second)) return nullptr;
|
||||
|
||||
auto& pool = elements.at(poolIt->second);
|
||||
auto& pool = elements[poolIt->second];
|
||||
auto entries = children_get(pool, Element::POOL_ENTRY);
|
||||
if (entries.empty()) return nullptr;
|
||||
|
||||
@@ -1346,6 +1380,34 @@ namespace game::resource::xml
|
||||
return dialogue_pool_entry_get(reference.poolID);
|
||||
}
|
||||
|
||||
Schema::Element* Schema::dialogue_hook_get(Element::Type type, int value)
|
||||
{
|
||||
auto* rootElement = root();
|
||||
auto* hooks = rootElement ? child_get(*rootElement, Element::HOOKS) : nullptr;
|
||||
if (!hooks) return nullptr;
|
||||
|
||||
Element* entryHook{};
|
||||
Element* poolHook{};
|
||||
for (auto* hook : children_get(*hooks, Element::HOOK))
|
||||
{
|
||||
if (!hook || type_get(hook->typeString.c_str()) != type) continue;
|
||||
if (value != 0 && hook->value == value && (!hook->idString.empty() || !hook->poolID.empty())) return hook;
|
||||
if (hook->value != 0) continue;
|
||||
if (!entryHook && !hook->idString.empty()) entryHook = hook;
|
||||
if (!poolHook && !hook->poolID.empty()) poolHook = hook;
|
||||
}
|
||||
|
||||
return entryHook ? entryHook : poolHook;
|
||||
}
|
||||
|
||||
Schema::Element* Schema::dialogue_hook_entry_get(Element::Type type, int value)
|
||||
{
|
||||
auto* hook = dialogue_hook_get(type, value);
|
||||
if (!hook) return nullptr;
|
||||
if (!hook->idString.empty()) return dialogue_entry_get(hook->idString);
|
||||
return dialogue_pool_entry_get(hook->poolID);
|
||||
}
|
||||
|
||||
int Schema::dialogue_entry_id_get(const std::string& id) const
|
||||
{
|
||||
auto it = entryIDMap.find(id);
|
||||
@@ -1367,7 +1429,7 @@ namespace game::resource::xml
|
||||
Schema::Element* Schema::item_get(const std::string& id)
|
||||
{
|
||||
auto it = itemIDMap.find(id);
|
||||
return it == itemIDMap.end() ? nullptr : &elements.at(it->second);
|
||||
return it == itemIDMap.end() || !element_index_is_valid(*this, it->second) ? nullptr : &elements[it->second];
|
||||
}
|
||||
|
||||
int Schema::item_id_get(const std::string& id) const
|
||||
|
||||
+27
-46
@@ -31,7 +31,6 @@
|
||||
X(Category, CATEGORY) \
|
||||
X(Character, CHARACTER) \
|
||||
X(CheatsActivated, CHEATS_ACTIVATED) \
|
||||
X(Chew, CHEW) \
|
||||
X(Choice, CHOICE) \
|
||||
X(Click, CLICK) \
|
||||
X(Close, CLOSE) \
|
||||
@@ -44,6 +43,7 @@
|
||||
X(DelayCharacters, DELAY_CHARACTERS) \
|
||||
X(Dialogue, DIALOGUE) \
|
||||
X(Digest, DIGEST) \
|
||||
X(Discovered, DISCOVERED) \
|
||||
X(Dispose, DISPOSE) \
|
||||
X(Eat, EAT) \
|
||||
X(EatArea, EAT_AREA) \
|
||||
@@ -74,6 +74,8 @@
|
||||
X(Help, HELP) \
|
||||
X(Home, HOME) \
|
||||
X(Hover, HOVER) \
|
||||
X(Hook, HOOK) \
|
||||
X(Hooks, HOOKS) \
|
||||
X(Hurt, HURT) \
|
||||
X(Idle, IDLE) \
|
||||
X(IdleFull, IDLE_FULL) \
|
||||
@@ -137,8 +139,6 @@
|
||||
X(Trigger, TRIGGER) \
|
||||
X(Triggers, TRIGGERS) \
|
||||
X(Unknown, UNKNOWN) \
|
||||
X(Upgrade, UPGRADE) \
|
||||
X(UpgradeFail, UPGRADE_FAIL) \
|
||||
X(Warning, WARNING) \
|
||||
X(Zoom, ZOOM)
|
||||
#define GAME_SCHEMA_ELEMENT_FIELDS(X, DATA) \
|
||||
@@ -163,6 +163,8 @@
|
||||
"", "", atFrame) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, baseAnm2, std::string, "Base ANM2", \
|
||||
STRING, "BaseAnm2", "", "", baseAnm2) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, baseSpawnDelayTicks, int, "Base Spawn Delay Ticks", INT_RW, \
|
||||
"BaseSpawnDelayTicks", "", "", baseSpawnDelayTicks) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, blinkChance, float, "Blink Chance", FLOAT, \
|
||||
"BlinkChance", "", "", blinkChance) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, blipDelayTicks, int, "Blip Delay Ticks", INT_RW, "BlipDelayTicks", "", "", \
|
||||
@@ -222,14 +224,6 @@
|
||||
"DigestionTimer", "", "", digestionTimer) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, digestionTimerMax, int, "Digestion Timer Max", INT_RW, \
|
||||
"DigestionTimerMax", "", "", digestionTimerMax) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, durability, int, "Durability", INT_RW, "Durability", \
|
||||
"", "", durability) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, eatSpeedMultiplier, float, "Eat Speed Multiplier", \
|
||||
FLOAT, "EatSpeedMultiplier", "", "", eatSpeedMultiplier) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, eatSpeedBonus, float, "Eat Speed Bonus", \
|
||||
FLOAT, "EatSpeedBonus", "", "", eatSpeedBonus) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, eatSpeedMaxMultiplier, float, "Eat Speed Max Multiplier", FLOAT, "EatSpeedMaxMultiplier", "", "", eatSpeedMaxMultiplier) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, eatSpeedMinMultiplier, float, "Eat Speed Min Multiplier", FLOAT, "EatSpeedMinMultiplier", "", "", eatSpeedMinMultiplier) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, end, int, "End", INT_RW, "End", \
|
||||
"", "", end) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, event, std::string, "Event To Activate", STRING, \
|
||||
@@ -270,16 +264,15 @@
|
||||
"", "", idleString) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, interpolated, bool, "Interpolated", BOOL, \
|
||||
"Interpolated", "", "", interpolated) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, itemSpawnLimit, int, "Item Spawn Limit", INT_RW, "ItemSpawnLimit", "", "", itemSpawnLimit) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, isAlternateSpritesheet, bool, "Is Alternate Spritesheet", BOOL, "IsAlternateSpritesheet", "", "", \
|
||||
isAlternateSpritesheet) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, isDigesting, bool, "Is Digesting", \
|
||||
BOOL, "IsDigesting", "", "", isDigesting) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, isDisableTalk, bool, "Is Disable Talk", BOOL, "IsDisableTalk", "", \
|
||||
"", isDisableTalk) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, isEdible, bool, "Is Edible", BOOL, "IsEdible", \
|
||||
"", "", isEdible) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, isHidden, bool, "Is Hidden", \
|
||||
BOOL, "IsHidden", "", "", isHidden) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, isHiddenInCatalog, bool, "Is Hidden In Catalog", BOOL, "IsHiddenInCatalog", "", "", \
|
||||
isHiddenInCatalog) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, isHold, bool, "Is Hold", BOOL, "IsHold", \
|
||||
"", "", isHold) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, isPostgame, bool, "Is Postgame", \
|
||||
@@ -287,6 +280,8 @@
|
||||
X(DATA, MEMBER_ATTRIBUTE, isToggleSpritesheet, bool, "Is Toggle Spritesheet", BOOL, "IsToggleSpritesheet", "", "", isToggleSpritesheet) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, isAudioMuted, bool, "Is Audio Muted", BOOL, \
|
||||
"IsAudioMuted", "", "", isAudioMuted) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, isDeveloperConsoleEnabled, bool, "Is Developer Console Enabled", BOOL, \
|
||||
"IsDeveloperConsoleEnabled", "", "", isDeveloperConsoleEnabled) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, isUseCharacterColor, bool, "Is Use Character Color", BOOL, "IsUseCharacterColor", "", "", isUseCharacterColor) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, layer, std::string, "Layer", STRING, \
|
||||
"Layer", "", "", layer) \
|
||||
@@ -319,10 +314,6 @@
|
||||
STRING_TARGET, "Portrait", "", "", portrait.path) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, position, glm::vec2, "Position", VEC2, "PositionX", \
|
||||
"PositionY", "", position) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, quantity, int, "Quantity", INT_RW, "Quantity", "", "", \
|
||||
quantity) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, quantityMax, int, "Quantity Max", INT_RW, \
|
||||
"QuantityMax", "", "", quantityMax) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, rarity, std::string, "Rarity", STRING, "Rarity", \
|
||||
"", "", rarity) \
|
||||
X(DATA, ATTRIBUTE, colorOffset.r, void, "Red Offset", \
|
||||
@@ -357,6 +348,9 @@
|
||||
X(DATA, MEMBER_ATTRIBUTE, spawnChanceBase, float, "Spawn Chance Base", \
|
||||
FLOAT, "SpawnChanceBase", "", "", spawnChanceBase) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, spawnChanceScoreBonus, float, "Spawn Chance Score Bonus", FLOAT, "SpawnChanceScoreBonus", "", "", spawnChanceScoreBonus) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, spawnDelayTicks, int, "Spawn Delay Ticks", INT_RW, "SpawnDelayTicks", "", "", spawnDelayTicks) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, spawnDelayStageMultiplier, float, "Spawn Delay Stage Multiplier", FLOAT, \
|
||||
"SpawnDelayStageMultiplier", "", "", spawnDelayStageMultiplier) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, spawnPadding, float, "Spawn Padding", FLOAT, \
|
||||
"SpawnPadding", "", "", spawnPadding) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, speed, float, "Speed", FLOAT, "Speed", "", "", speed) \
|
||||
@@ -391,14 +385,14 @@
|
||||
X(DATA, MEMBER_ATTRIBUTE, title, std::string, "Title", STRING, "Title", \
|
||||
"", "", title) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, totalCaloriesConsumed, float, "Total Calories Consumed", FLOAT, "TotalCaloriesConsumed", "", "", totalCaloriesConsumed) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, totalFoodItemsEaten, int, "Total Food Items Eaten", \
|
||||
INT_RW, "TotalFoodItemsEaten", "", "", totalFoodItemsEaten) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, totalItemsConsumed, int, "Total Items Consumed", INT_RW, "TotalItemsConsumed", "", "", \
|
||||
totalItemsConsumed) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, totalItemsSpawned, int, "Total Items Spawned", INT_RW, "TotalItemsSpawned", "", "", \
|
||||
totalItemsSpawned) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, totalPlaytimeSeconds, float, "Total Playtime Seconds", FLOAT, "TotalPlaytimeSeconds", "", "", totalPlaytimeSeconds) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, typeString, std::string, "Type", STRING, "Type", "", \
|
||||
"", typeString) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, upgradeCount, int, "Upgrade Count", INT_RW, \
|
||||
"UpgradeCount", "", "", upgradeCount) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, upgradeID, std::string, "Upgrade ID", \
|
||||
STRING, "UpgradeID", "", "", upgradeID) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, useMode, std::string, "Use Mode", STRING, "UseMode", "", "", useMode) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, value, int, "Value", INT_RW, "Value", "", "", value) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, velocity, glm::vec2, "Velocity", VEC2, "VelocityX", \
|
||||
"VelocityY", "", velocity) \
|
||||
@@ -416,20 +410,20 @@
|
||||
"", windowSize) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, windowPosition, glm::vec2, "Window Position", VEC2, "WindowX", \
|
||||
"WindowY", "", windowPosition) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, worldBoundsNull, std::string, "World Bounds Null", STRING, "WorldBoundsNull", "", "", worldBoundsNull) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, itemSpawnNull, std::string, "Item Spawn Null", STRING, "ItemSpawnNull", "", "", itemSpawnNull) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, crop, glm::vec2, "Crop", VEC2, "XCrop", "YCrop", "", crop) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, pivot, glm::vec2, "Pivot", VEC2, "XPivot", "YPivot", "", pivot) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, framePosition, glm::vec2, "Frame Position", VEC2, \
|
||||
"XPosition", "YPosition", "", framePosition) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, scale, glm::vec2, "Scale", VEC2, "XScale", "YScale", "", scale) \
|
||||
X(DATA, MEMBER_ATTRIBUTE, zoom, float, "Zoom", FLOAT, "Zoom", "", "", zoom) \
|
||||
X(DATA, MEMBER, animationChew, std::string, "Animation Chew", NONE, "", \
|
||||
"", "", animationChew) \
|
||||
X(DATA, MEMBER, animationClick, AnimationEntryCollection, "Animation Click", NONE, "", "", "", animationClick) \
|
||||
X(DATA, MEMBER, animationGrab, AnimationEntryCollection, "Animation Grab", NONE, "", "", "", animationGrab) \
|
||||
X(DATA, MEMBER, animationHover, AnimationEntryCollection, "Animation Hover", NONE, "", "", "", animationHover) \
|
||||
X(DATA, MEMBER, animationIdle, AnimationEntryCollection, "Animation Idle", NONE, "", "", "", animationIdle) \
|
||||
X(DATA, MEMBER, animationPan, AnimationEntryCollection, "Animation Pan", NONE, "", "", "", animationPan) \
|
||||
X(DATA, MEMBER, animationReturn, AnimationEntryCollection, "Animation Return", NONE, "", "", "", animationReturn) \
|
||||
X(DATA, MEMBER, animationDispose, AnimationEntryCollection, "Animation Dispose", NONE, "", "", "", animationDispose) \
|
||||
X(DATA, MEMBER, animationZoom, AnimationEntryCollection, "Animation Zoom", NONE, "", "", "", animationZoom) \
|
||||
X(DATA, MEMBER, anm2Entry, Anm2Entry, "ANM2 Entry", NONE, "", "", "", \
|
||||
anm2Entry) \
|
||||
@@ -478,17 +472,12 @@
|
||||
soundOpen) \
|
||||
X(DATA, MEMBER, soundRelease, SoundEntryCollection, "Sound Release", NONE, \
|
||||
"", "", "", soundRelease) \
|
||||
X(DATA, MEMBER, soundReturn, SoundEntryCollection, "Sound Return", NONE, "", \
|
||||
"", "", soundReturn) \
|
||||
X(DATA, MEMBER, soundSelect, SoundEntryCollection, "Sound Select", NONE, "", \
|
||||
"", "", soundSelect) \
|
||||
X(DATA, MEMBER, soundSummon, SoundEntryCollection, "Sound Summon", NONE, "", \
|
||||
"", "", soundSummon) \
|
||||
X(DATA, MEMBER, soundThrow, SoundEntryCollection, "Sound Throw", NONE, "", "", \
|
||||
"", soundThrow) \
|
||||
X(DATA, MEMBER, soundUpgrade, SoundEntryCollection, "Sound Upgrade", NONE, \
|
||||
"", "", "", soundUpgrade) \
|
||||
X(DATA, MEMBER, soundUpgradeFail, SoundEntryCollection, "Sound Upgrade Fail", NONE, "", "", "", soundUpgradeFail) \
|
||||
X(DATA, MEMBER, textureEntry, TextureEntry, "Texture Entry", NONE, "", \
|
||||
"", "", textureEntry) \
|
||||
X(DATA, MEMBER, tint, glm::vec4, "Tint", NONE, "", "", "", tint) \
|
||||
@@ -562,7 +551,7 @@ namespace game::resource::xml
|
||||
std::string* get();
|
||||
};
|
||||
|
||||
struct SoundEntryCollection : std::vector<SoundEntry*>
|
||||
struct SoundEntryCollection : std::vector<SoundEntry>
|
||||
{
|
||||
const Audio* get() const;
|
||||
void play() const;
|
||||
@@ -580,14 +569,14 @@ namespace game::resource::xml
|
||||
struct Category
|
||||
{
|
||||
std::string name{};
|
||||
bool isEdible{};
|
||||
std::string useMode{};
|
||||
};
|
||||
|
||||
struct Rarity
|
||||
{
|
||||
std::string name{};
|
||||
float weight{};
|
||||
bool isHidden{};
|
||||
bool isHiddenInCatalog{};
|
||||
Audio sound{};
|
||||
};
|
||||
|
||||
@@ -607,22 +596,14 @@ namespace game::resource::xml
|
||||
bool isCalories{};
|
||||
bool isCapacityBonus{};
|
||||
bool isDigestionBonus{};
|
||||
bool isEatSpeedBonus{};
|
||||
bool isGravity{};
|
||||
bool isDurability{};
|
||||
bool isColor{};
|
||||
bool isUpgradeID{};
|
||||
bool isUpgradeCount{};
|
||||
bool isToggleSpritesheet{};
|
||||
float calories{};
|
||||
float capacityBonus{};
|
||||
float digestionBonus{};
|
||||
float eatSpeedBonus{};
|
||||
float gravity{};
|
||||
int durability{};
|
||||
glm::vec3 color{};
|
||||
int upgradeID{-1};
|
||||
int upgradeCount{};
|
||||
};
|
||||
|
||||
struct Element
|
||||
@@ -693,8 +674,6 @@ namespace game::resource::xml
|
||||
Anm2 baseAnm2{};
|
||||
Font font{};
|
||||
Font iconsFont{};
|
||||
int durability{2};
|
||||
int quantityMax{99};
|
||||
float rounding{};
|
||||
|
||||
Schema() = default;
|
||||
@@ -732,6 +711,8 @@ namespace game::resource::xml
|
||||
Element* dialogue_pool_entry_get(int poolID);
|
||||
Element* dialogue_pool_entry_get(const std::string& poolID);
|
||||
Element* dialogue_pool_entry_get(Element& reference);
|
||||
Element* dialogue_hook_get(Element::Type type, int value = 0);
|
||||
Element* dialogue_hook_entry_get(Element::Type type, int value = 0);
|
||||
int dialogue_entry_id_get(const std::string& id) const;
|
||||
int dialogue_pool_id_get(const std::string& id) const;
|
||||
Element* item_get(int id);
|
||||
|
||||
@@ -9,24 +9,24 @@ namespace game::resource::xml
|
||||
{
|
||||
#define GAME_XML_STRING_LIST(X) \
|
||||
X(MenuTabInteract, "TextMenuTabInteract", "Interact") \
|
||||
X(MenuTabGather, "TextMenuTabGather", "Gather") \
|
||||
X(MenuTabInventory, "TextMenuTabInventory", "Inventory") \
|
||||
X(MenuTabInventory, "TextMenuTabInventory", "Catalog") \
|
||||
X(MenuTabSettings, "TextMenuTabSettings", "Settings") \
|
||||
X(MenuTabCheats, "TextMenuTabCheats", "Cheats") \
|
||||
X(MenuOpenTooltip, "TextMenuOpenTooltip", "Open Main Menu") \
|
||||
X(MenuOpenFullscreenTooltip, "TextMenuOpenFullscreenTooltip", "Open Fullscreen Main Menu") \
|
||||
X(MenuRestoreTooltip, "TextMenuRestoreTooltip", "Restore Main Menu") \
|
||||
X(MenuCloseTooltip, "TextMenuCloseTooltip", "Close Main Menu") \
|
||||
X(InteractChatButton, "TextInteractChatButton", "Let's chat!") \
|
||||
X(InteractHelpButton, "TextInteractHelpButton", "Help") \
|
||||
X(InteractFeelingButton, "TextInteractFeelingButton", "How are you feeling?") \
|
||||
X(InteractSpawnItemButton, "TextInteractSpawnItemButton", "Spawn Item") \
|
||||
X(InteractSpawnItemCooldownFormat, "TextInteractSpawnItemCooldownFormat", "Next item in %0.2fs") \
|
||||
X(InteractStats, "TextInteractStats", "Stats") \
|
||||
X(InteractWeightFormat, "TextInteractWeightFormat", "Weight: %0.2f %s (Stage: %i)") \
|
||||
X(InteractCapacityFormat, "TextInteractCapacityFormat", "Capacity: %0.0f kcal (Max: %0.0f kcal)") \
|
||||
X(InteractDigestionRateFormat, "TextInteractDigestionRateFormat", "Digestion Rate: %0.2f%%/sec") \
|
||||
X(InteractEatingSpeedFormat, "TextInteractEatingSpeedFormat", "Eating Speed: %0.2fx") \
|
||||
X(InteractTotalCaloriesFormat, "TextInteractTotalCaloriesFormat", "Total Calories Consumed: %0.0f kcal") \
|
||||
X(InteractTotalFoodItemsFormat, "TextInteractTotalFoodItemsFormat", "Total Food Items Eaten: %i") \
|
||||
X(InteractTotalItemsSpawnedFormat, "TextInteractTotalItemsSpawnedFormat", "Total Items Spawned: %i") \
|
||||
X(InteractTotalItemsConsumedFormat, "TextInteractTotalItemsConsumedFormat", "Total Items Consumed: %i") \
|
||||
X(InteractTotalPlaytimeFormat, "TextInteractTotalPlaytimeFormat", "Total Playtime: %s") \
|
||||
X(SettingsWindow, "TextSettingsWindow", "Window") \
|
||||
X(SettingsWindowMode, "TextSettingsWindowMode", "Mode") \
|
||||
X(SettingsWindowModeTooltip, "TextSettingsWindowModeTooltip", \
|
||||
@@ -61,32 +61,20 @@ namespace game::resource::xml
|
||||
X(ToolsHomeTooltip, "TextToolsHomeTooltip", "Reset camera view.\n(Shortcut: Home)") \
|
||||
X(ToolsOpenTooltip, "TextToolsOpenTooltip", "Open Tools") \
|
||||
X(ToolsCloseTooltip, "TextToolsCloseTooltip", "Close Tools") \
|
||||
X(InventoryEmptyHint, "TextInventoryEmptyHint", "No items yet.") \
|
||||
X(InventoryEmptyHint, "TextInventoryEmptyHint", "No items.") \
|
||||
X(InventoryFlavorFormat, "TextInventoryFlavorFormat", "Flavor: %s") \
|
||||
X(InventoryCaloriesFormat, "TextInventoryCaloriesFormat", "%0.0f kcal") \
|
||||
X(InventoryDurabilityFormat, "TextInventoryDurabilityFormat", "Durability: %i") \
|
||||
X(InventoryCapacityBonusFormat, "TextInventoryCapacityBonusFormat", "Capacity Bonus: +%0.0f kcal") \
|
||||
X(InventoryDigestionRateBonusFormat, "TextInventoryDigestionRateBonusFormat", \
|
||||
"Digestion Rate Bonus: +%0.2f%% / sec") \
|
||||
X(InventoryDigestionRatePenaltyFormat, "TextInventoryDigestionRatePenaltyFormat", \
|
||||
"Digestion Rate Penalty: %0.2f%% / sec") \
|
||||
X(InventoryEatSpeedBonusFormat, "TextInventoryEatSpeedBonusFormat", "Eat Speed Bonus: +%0.2f%% / sec") \
|
||||
X(InventoryEatSpeedPenaltyFormat, "TextInventoryEatSpeedPenaltyFormat", "Eat Speed Penalty: %0.2f%% / sec") \
|
||||
X(InventoryUpgradePreviewFormat, "TextInventoryUpgradePreviewFormat", "Upgrade: %ix -> %s") \
|
||||
X(InventoryUnknown, "TextInventoryUnknown", "???") \
|
||||
X(InventorySpawnButton, "TextInventorySpawnButton", "Spawn") \
|
||||
X(InventoryEnableAutofeedButton, "TextInventoryEnableAutofeedButton", "Enable Autofeed") \
|
||||
X(InventoryDisableAutofeedButton, "TextInventoryDisableAutofeedButton", "Disable Autofeed") \
|
||||
X(InventoryEnableAutofeedTooltip, "TextInventoryEnableAutofeedTooltip", \
|
||||
"Serve the character food and assist in digestion automatically.") \
|
||||
X(InventoryDisableAutofeedTooltip, "TextInventoryDisableAutofeedTooltip", \
|
||||
"Return to manual interaction.") \
|
||||
X(InventoryUpgradeButton, "TextInventoryUpgradeButton", "Upgrade") \
|
||||
X(InventoryUpgradeAllButton, "TextInventoryUpgradeAllButton", "Upgrade All") \
|
||||
X(InventoryUpgradeNoPath, "TextInventoryUpgradeNoPath", "This item cannot be upgraded.") \
|
||||
X(InventoryUpgradeNeedsTemplate, "TextInventoryUpgradeNeedsTemplate", "Needs {}x to upgrade into {}!") \
|
||||
X(InventoryUpgradeOneTemplate, "TextInventoryUpgradeOneTemplate", "Use {}x to upgrade into 1x {}.") \
|
||||
X(InventoryUpgradeAllTemplate, "TextInventoryUpgradeAllTemplate", "Use {}x to upgrade into {}x {}.") \
|
||||
X(InfoProgressMax, "TextInfoProgressMax", "MAX") \
|
||||
X(InfoProgressToNextStage, "TextInfoProgressToNextStage", "To Next Stage") \
|
||||
X(InfoStageProgressFormat, "TextInfoStageProgressFormat", "Stage: %i/%i (%0.1f%%)") \
|
||||
@@ -99,7 +87,6 @@ namespace game::resource::xml
|
||||
X(InfoDigestionInProgress, "TextInfoDigestionInProgress", "Digestion in progress...") \
|
||||
X(InfoGiveFoodToStartDigesting, "TextInfoGiveFoodToStartDigesting", "Give food to start digesting!") \
|
||||
X(InfoDigestionRateFormat, "TextInfoDigestionRateFormat", "Rate: %0.2f%% / sec") \
|
||||
X(InfoEatingSpeedFormat, "TextInfoEatingSpeedFormat", "Eating Speed: %0.2fx") \
|
||||
X(CheatsCalories, "TextCheatsCalories", "Calories") \
|
||||
X(CheatsCapacity, "TextCheatsCapacity", "Capacity") \
|
||||
X(CheatsWeight, "TextCheatsWeight", "Weight") \
|
||||
@@ -107,10 +94,7 @@ namespace game::resource::xml
|
||||
X(CheatsStage, "TextCheatsStage", "Stage") \
|
||||
X(CheatsDigestionRate, "TextCheatsDigestionRate", "Digestion Rate") \
|
||||
X(CheatsDigestionRateFormat, "TextCheatsDigestionRateFormat", "%0.2f%% / sec") \
|
||||
X(CheatsEatSpeed, "TextCheatsEatSpeed", "Eat Speed") \
|
||||
X(CheatsEatSpeedFormat, "TextCheatsEatSpeedFormat", "%0.2fx") \
|
||||
X(CheatsDigestButton, "TextCheatsDigestButton", "Digest") \
|
||||
X(CheatsInventory, "TextCheatsInventory", "Inventory")
|
||||
X(CheatsDigestButton, "TextCheatsDigestButton", "Digest")
|
||||
|
||||
class Strings
|
||||
{
|
||||
|
||||
+7
-1
@@ -39,7 +39,7 @@ namespace game
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> archives{};
|
||||
for (auto& entry : std::filesystem::recursive_directory_iterator(CHARACTERS_DIRECTORY, ec))
|
||||
for (auto& entry : std::filesystem::directory_iterator(CHARACTERS_DIRECTORY, ec))
|
||||
if (!ec && entry.is_regular_file(ec) && entry.path().extension() == ".zip") archives.emplace_back(entry.path());
|
||||
|
||||
std::ranges::sort(archives);
|
||||
@@ -47,6 +47,12 @@ namespace game
|
||||
{
|
||||
auto& preview = characterPreviews.emplace_back();
|
||||
preview.load_archive(path, "character.xml");
|
||||
if (!preview.root())
|
||||
{
|
||||
characterPreviews.pop_back();
|
||||
logger.warning(std::format("Skipping invalid character archive: {}", path.string()));
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& character = characters.emplace_back(path);
|
||||
characterSaves.emplace_back(character.save_path_get());
|
||||
|
||||
+21
-20
@@ -65,13 +65,14 @@ namespace game
|
||||
resources.audio_mute_set(settingsRoot->isAudioMuted);
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
else if (event.type == SDL_EVENT_DROP_FILE)
|
||||
{
|
||||
resources.isDeveloper = true;
|
||||
developerWindow.dropped_file_open(event.drop.data);
|
||||
if (auto* settingsRoot = resources.settings.root(); settingsRoot && settingsRoot->isDeveloperConsoleEnabled)
|
||||
{
|
||||
resources.isDeveloper = true;
|
||||
developerWindow.dropped_file_open(event.drop.data);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!isRunning) return;
|
||||
}
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
@@ -110,22 +111,22 @@ namespace game
|
||||
break;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
developerWindow.update(resources, window, type == PLAY ? &play : nullptr);
|
||||
if (developerWindow.action.isMenu && type == PLAY)
|
||||
{
|
||||
play.exit(resources);
|
||||
type = SELECT;
|
||||
resources.last_character_clear();
|
||||
}
|
||||
if (developerWindow.action.character >= 0)
|
||||
{
|
||||
if (type == PLAY) play.exit(resources);
|
||||
select.characterIndex = developerWindow.action.character;
|
||||
play.set(resources, select.characterIndex, state::Play::CONTINUE);
|
||||
type = PLAY;
|
||||
}
|
||||
#endif
|
||||
developerWindow.update(resources, window, type == PLAY ? &play : nullptr);
|
||||
if (developerWindow.action.isMenu && type == PLAY)
|
||||
{
|
||||
play.exit(resources);
|
||||
type = SELECT;
|
||||
resources.last_character_clear();
|
||||
}
|
||||
if (developerWindow.action.character >= 0)
|
||||
{
|
||||
if (type == PLAY) play.exit(resources);
|
||||
select.characterIndex = developerWindow.action.character;
|
||||
if (developerWindow.action.isNewGame) resources.character_save_clear(select.characterIndex);
|
||||
play.set(resources, select.characterIndex,
|
||||
developerWindow.action.isNewGame ? state::Play::NEW_GAME : state::Play::CONTINUE);
|
||||
type = PLAY;
|
||||
}
|
||||
|
||||
auto isHideCursor = type == PLAY && !resources.isDeveloper && play.cursor_enabled_get();
|
||||
if (isHideCursor != isCursorHidden)
|
||||
|
||||
@@ -5,9 +5,7 @@
|
||||
#include "render/canvas.hpp"
|
||||
#include "resources.hpp"
|
||||
|
||||
#if DEBUG
|
||||
#include "developer/window.hpp"
|
||||
#endif
|
||||
|
||||
#include "state/play.hpp"
|
||||
#include "state/select.hpp"
|
||||
@@ -35,9 +33,7 @@ namespace game
|
||||
|
||||
state::Play play;
|
||||
state::Select select;
|
||||
#if DEBUG
|
||||
developer::Window developerWindow;
|
||||
#endif
|
||||
|
||||
void update();
|
||||
void render();
|
||||
|
||||
+100
-142
@@ -19,23 +19,24 @@ using namespace glm;
|
||||
|
||||
namespace game::state
|
||||
{
|
||||
namespace
|
||||
void entity_debug_flags_apply(Entity& entity, bool isShowLayers, bool isShowNulls)
|
||||
{
|
||||
int durability_animation_index_get(const resource::xml::Schema& schema, const resource::xml::Anm2& anm2,
|
||||
int durability, int durabilityMax)
|
||||
{
|
||||
if (durability >= durabilityMax) return -1;
|
||||
entity.isShowLayers = isShowLayers;
|
||||
entity.isShowNulls = isShowNulls;
|
||||
}
|
||||
|
||||
auto animationName = schema.root()->animationChew + std::to_string(std::max(0, durability));
|
||||
return anm2.animationMap.contains(animationName) ? anm2.animationMap.at(animationName) : -1;
|
||||
}
|
||||
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 (!isWindows) return World::CENTER;
|
||||
if (text.is_scene_active()) return World::CENTER;
|
||||
|
||||
auto isToolsOpen = tools.isOpen && !menu.is_fullscreen_visible_get();
|
||||
auto isToolsOpen = tools.isOpen;
|
||||
return menu.isOpen && isToolsOpen ? World::MENU_TOOLS
|
||||
: menu.isOpen ? World::MENU
|
||||
: isToolsOpen ? World::TOOLS
|
||||
@@ -48,34 +49,16 @@ namespace game::state
|
||||
return root && root->type == Schema::Element::CURSOR;
|
||||
}
|
||||
|
||||
void Play::start_sequence_begin()
|
||||
void Play::debug_flags_apply()
|
||||
{
|
||||
auto& dialogue = character.data.dialogue;
|
||||
auto* start = dialogue.get(Schema::Element::START);
|
||||
if (!start) return;
|
||||
|
||||
character.play({.animation = start->animation,
|
||||
.appendID = character.animation_append_id_get(),
|
||||
.interrupt = Entity::Interrupt::NEVER});
|
||||
character.update();
|
||||
text.entry = nullptr;
|
||||
text.isEnabled = false;
|
||||
isWindows = false;
|
||||
isStart = true;
|
||||
isStartBegin = false;
|
||||
isStartEnd = false;
|
||||
}
|
||||
|
||||
void Play::end_sequence_begin()
|
||||
{
|
||||
auto& dialogue = character.data.dialogue;
|
||||
if (!dialogue.get(Schema::Element::END)) return;
|
||||
|
||||
text.entry = nullptr;
|
||||
text.isEnabled = false;
|
||||
isEnd = true;
|
||||
isEndBegin = false;
|
||||
isEndEnd = false;
|
||||
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)
|
||||
@@ -84,8 +67,8 @@ namespace game::state
|
||||
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* saveInventory = isSaveValid ? data.save.child_get(*saveRoot, Schema::Element::INVENTORY) : 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;
|
||||
@@ -96,12 +79,11 @@ namespace game::state
|
||||
resources.last_character_save(selectedCharacterIndex);
|
||||
cheatCodeIndex = 0;
|
||||
cheatCodeStartTime = 0.0;
|
||||
world = play::World{};
|
||||
|
||||
character = Entity(data, vec2(World::BOUNDS.x + World::BOUNDS.z * 0.5f, World::BOUNDS.w - World::BOUNDS.y));
|
||||
character = Entity(data, vec2{});
|
||||
character.digestionRate =
|
||||
glm::clamp(character.digestionRate, characterRoot->digestionRateMin, characterRoot->digestionRateMax);
|
||||
character.eatSpeed =
|
||||
glm::clamp(character.eatSpeed, characterRoot->eatSpeedMinMultiplier, characterRoot->eatSpeedMaxMultiplier);
|
||||
character.capacity =
|
||||
glm::clamp(character.capacity, characterRoot->capacityMinCalories, characterRoot->capacityMaxCalories);
|
||||
|
||||
@@ -116,11 +98,16 @@ namespace game::state
|
||||
}
|
||||
|
||||
character.totalCaloriesConsumed = saveCharacter ? saveCharacter->totalCaloriesConsumed : 0;
|
||||
character.totalFoodItemsEaten = saveCharacter ? saveCharacter->totalFoodItemsEaten : 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;
|
||||
@@ -143,24 +130,24 @@ namespace game::state
|
||||
}
|
||||
|
||||
menu.inventory = play::menu::Inventory{};
|
||||
for (auto* item : saveInventory ? data.save.children_get(*saveInventory, Schema::Element::ITEM)
|
||||
: std::vector<Schema::Element*>{})
|
||||
{
|
||||
if (item->quantity == 0) continue;
|
||||
menu.inventory.values[item->id] = item->quantity;
|
||||
}
|
||||
|
||||
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<Schema::Element*>{})
|
||||
{
|
||||
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<Schema::Element*>{})
|
||||
{
|
||||
auto& anm2 = itemSchema.anm2s.at(item->id);
|
||||
auto& schemaItem = itemSchema.items.at(item->id);
|
||||
auto durabilityMax = schemaItem.isDurability ? schemaItem.durability : itemSchema.durability;
|
||||
auto animationIndex = durability_animation_index_get(itemSchema, anm2, item->durability, durabilityMax);
|
||||
auto& saveItem = itemSchema.anm2s.at(item->id);
|
||||
itemManager.items.emplace_back(saveItem, item->position, item->id, item->durability, animationIndex,
|
||||
item->velocity, item->rotation);
|
||||
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);
|
||||
@@ -169,6 +156,7 @@ namespace game::state
|
||||
|
||||
text.entry = nullptr;
|
||||
text.isEnabled = false;
|
||||
isPostgamePending = false;
|
||||
|
||||
#if DEBUG
|
||||
menu.isCheats = true;
|
||||
@@ -180,32 +168,29 @@ namespace game::state
|
||||
if (character.stage_get() >= character.stage_max_get()) isPostgame = true;
|
||||
if (isPostgame) menu.isCheats = true;
|
||||
|
||||
if (game == NEW_GAME) isWindows = false;
|
||||
|
||||
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();
|
||||
areaManager.set(character);
|
||||
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 && dialogue.get(Schema::Element::START)) start_sequence_begin();
|
||||
if (game == NEW_GAME)
|
||||
text.set(dialogue.dialogue_hook_entry_get(Schema::Element::START), character, false);
|
||||
|
||||
if (isPostgame)
|
||||
{
|
||||
isEnd = true;
|
||||
isEndBegin = true;
|
||||
isEndEnd = true;
|
||||
isPostgamePending = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
isEnd = false;
|
||||
isEndBegin = false;
|
||||
isEndEnd = false;
|
||||
isPostgamePending = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +216,7 @@ namespace game::state
|
||||
auto focus = focus_get();
|
||||
auto& dialogue = character.data.dialogue;
|
||||
auto isCursorEnabled = cursor_enabled_get();
|
||||
character.totalPlaytimeSeconds += ImGui::GetIO().DeltaTime;
|
||||
if (isCursorEnabled)
|
||||
{
|
||||
cursor.isVisible = true;
|
||||
@@ -276,87 +262,55 @@ namespace game::state
|
||||
}
|
||||
}
|
||||
|
||||
if (isWindows)
|
||||
auto isSceneActive = text.is_scene_active();
|
||||
|
||||
if (!isSceneActive)
|
||||
{
|
||||
auto isToolsDisabled = menu.is_fullscreen_visible_get();
|
||||
auto uiAlpha = UI_ALPHA_MAX - menu.fullscreen_alpha_get();
|
||||
tools.update(character, cursor, world, focus, worldCanvas, isToolsDisabled, uiAlpha);
|
||||
info.update(resources, character, uiAlpha);
|
||||
tools.update(character, cursor, world, focus, worldCanvas, false, UI_ALPHA_MAX);
|
||||
menu.update(resources, itemManager, character, text, autofeed);
|
||||
toasts.update();
|
||||
}
|
||||
|
||||
auto isEndSequenceActive = isEndBegin && !isEndEnd;
|
||||
itemManager.isDisabled = isEndSequenceActive;
|
||||
characterManager.isDisabled = isEndSequenceActive;
|
||||
itemManager.isDisabled = isSceneActive;
|
||||
characterManager.isDisabled = isSceneActive;
|
||||
|
||||
if (text.isEnabled) text.update(character, worldCanvas);
|
||||
|
||||
if (isStart)
|
||||
if (character.isJustStageFinal && !isPostgame && !isPostgamePending)
|
||||
{
|
||||
if (!isStartBegin)
|
||||
auto* end = dialogue.dialogue_hook_entry_get(Schema::Element::END);
|
||||
if (end)
|
||||
{
|
||||
if (auto animation = character.animation_get())
|
||||
{
|
||||
if (animation->isLoop || character.state == Entity::STOPPED)
|
||||
{
|
||||
auto* start = dialogue.get(Schema::Element::START);
|
||||
text.set(start ? dialogue.dialogue_entry_get(*start) : nullptr, character);
|
||||
isStartBegin = true;
|
||||
}
|
||||
}
|
||||
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 if (!isStartEnd)
|
||||
else
|
||||
{
|
||||
if (text.entry && text.is_terminal() && text.is_finished())
|
||||
{
|
||||
isWindows = true;
|
||||
isStartEnd = true;
|
||||
isStart = false;
|
||||
world.character_focus(character, worldCanvas, focus_get());
|
||||
}
|
||||
isPostgame = true;
|
||||
menu.isCheats = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (character.isJustStageFinal && !isEnd && !isPostgame) isEnd = true;
|
||||
|
||||
if (isEnd)
|
||||
if (isPostgamePending && text.is_terminal() && text.is_finished() && !text.is_scene_active())
|
||||
{
|
||||
if (!isEndBegin)
|
||||
{
|
||||
if (character.is_animation_finished())
|
||||
{
|
||||
auto* end = dialogue.get(Schema::Element::END);
|
||||
text.set(end ? dialogue.dialogue_entry_get(*end) : nullptr, character);
|
||||
isEndBegin = true;
|
||||
isWindows = false;
|
||||
tools.isOpen = false;
|
||||
menu.isOpen = false;
|
||||
character.calories = 0;
|
||||
character.digestionProgress = 0;
|
||||
itemManager.heldItemIndex = -1;
|
||||
world.character_focus(character, worldCanvas, focus_get());
|
||||
}
|
||||
}
|
||||
else if (!isEndEnd)
|
||||
{
|
||||
if (text.entry && text.is_terminal() && text.is_finished())
|
||||
{
|
||||
menu.isOpen = true;
|
||||
isWindows = true;
|
||||
isEndEnd = true;
|
||||
isEnd = false;
|
||||
isPostgame = true;
|
||||
menu.isCheats = true;
|
||||
world.character_focus(character, worldCanvas, focus_get());
|
||||
}
|
||||
}
|
||||
menu.isOpen = true;
|
||||
isPostgamePending = false;
|
||||
isPostgame = true;
|
||||
menu.isCheats = true;
|
||||
world.character_focus(character, worldCanvas, focus_get());
|
||||
}
|
||||
|
||||
autofeed.update(resources, character, autofeedCursor, menu.inventory, itemManager, characterManager, worldCanvas);
|
||||
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);
|
||||
itemManager.update(character, interactionCursor, areaManager, text, world.bounds, worldCanvas);
|
||||
characterManager.update(character, interactionCursor, text, worldCanvas);
|
||||
for (auto& particle : itemManager.particles)
|
||||
particleManager.spawn(character.data, particle.label, particle.position);
|
||||
@@ -371,6 +325,7 @@ namespace game::state
|
||||
|
||||
character.update();
|
||||
areaManager.update(character);
|
||||
world.bounds_set(areaManager.bounds_get(character, world.bounds));
|
||||
particleManager.update();
|
||||
cursor.update();
|
||||
if (autofeed.active_get()) autofeedCursor.update();
|
||||
@@ -396,13 +351,15 @@ namespace game::state
|
||||
worldCanvas.bind();
|
||||
worldCanvas.size_set(size);
|
||||
worldCanvas.clear();
|
||||
debug_flags_apply();
|
||||
|
||||
areaManager.render(character, textureShader, rectShader, worldCanvas);
|
||||
|
||||
character.render(textureShader, rectShader, worldCanvas);
|
||||
|
||||
for (auto& item : itemManager.items)
|
||||
item.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);
|
||||
@@ -423,9 +380,11 @@ namespace game::state
|
||||
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;
|
||||
@@ -438,36 +397,35 @@ namespace game::state
|
||||
characterElement.calories = character.calories;
|
||||
characterElement.capacityCalories = character.capacity;
|
||||
characterElement.digestionRate = character.digestionRate;
|
||||
characterElement.eatSpeedMultiplier = character.eatSpeed;
|
||||
characterElement.digestionProgress = character.digestionProgress;
|
||||
characterElement.isDigesting = character.isDigesting;
|
||||
characterElement.digestionTimer = character.digestionTimer;
|
||||
characterElement.totalCaloriesConsumed = character.totalCaloriesConsumed;
|
||||
characterElement.totalFoodItemsEaten = character.totalFoodItemsEaten;
|
||||
|
||||
auto inventoryIndex = (int)save.elements.size();
|
||||
save.element_add(resource::xml::Schema::Element::INVENTORY, 0);
|
||||
|
||||
for (auto& [id, quantity] : menu.inventory.values)
|
||||
{
|
||||
if (quantity == 0) continue;
|
||||
auto& item = save.element_add(resource::xml::Schema::Element::ITEM, inventoryIndex);
|
||||
item.id = id;
|
||||
item.quantity = quantity;
|
||||
}
|
||||
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.durability = item.durability;
|
||||
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());
|
||||
|
||||
|
||||
+4
-14
@@ -5,7 +5,6 @@
|
||||
#include "play/area_manager.hpp"
|
||||
#include "play/autofeed.hpp"
|
||||
#include "play/character_manager.hpp"
|
||||
#include "play/info.hpp"
|
||||
#include "play/item_manager.hpp"
|
||||
#include "play/menu.hpp"
|
||||
#include "play/particle_manager.hpp"
|
||||
@@ -31,7 +30,6 @@ namespace game::state
|
||||
Entity cursor;
|
||||
Entity autofeedCursor;
|
||||
|
||||
play::Info info;
|
||||
play::Menu menu;
|
||||
play::Tools tools;
|
||||
play::Text text;
|
||||
@@ -50,17 +48,10 @@ namespace game::state
|
||||
int cheatCodeIndex{};
|
||||
double cheatCodeStartTime{};
|
||||
|
||||
bool isWindows{true};
|
||||
|
||||
bool isStartBegin{};
|
||||
bool isStart{};
|
||||
bool isStartEnd{};
|
||||
|
||||
bool isEndBegin{};
|
||||
bool isEnd{};
|
||||
bool isEndEnd{};
|
||||
|
||||
bool isPostgame{};
|
||||
bool isPostgamePending{};
|
||||
bool isShowLayers{};
|
||||
bool isShowNulls{};
|
||||
|
||||
Canvas worldCanvas{play::World::SIZE};
|
||||
|
||||
@@ -71,8 +62,7 @@ namespace game::state
|
||||
void update(Resources&);
|
||||
void render(Resources&, Canvas&);
|
||||
void save(Resources&);
|
||||
void debug_flags_apply();
|
||||
play::World::Focus focus_get();
|
||||
void start_sequence_begin();
|
||||
void end_sequence_begin();
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,34 @@
|
||||
#include "area_manager.hpp"
|
||||
|
||||
#include "../../util/math.hpp"
|
||||
#include <cmath>
|
||||
|
||||
using namespace game::resource;
|
||||
using namespace game::util;
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr auto DEFAULT_WORLD_BOUNDS_NULL = "Bounds";
|
||||
constexpr auto ZERO_FLOAT = 0.0f;
|
||||
|
||||
bool rect_is_valid(glm::vec4 rect)
|
||||
{
|
||||
return std::isfinite(rect.x) && std::isfinite(rect.y) && std::isfinite(rect.z) && std::isfinite(rect.w) &&
|
||||
rect.z > ZERO_FLOAT && rect.w > ZERO_FLOAT;
|
||||
}
|
||||
|
||||
glm::vec4 bounds_from_rect_get(glm::vec4 rect)
|
||||
{
|
||||
return {rect.x, rect.y, rect.x + rect.z, rect.y + rect.w};
|
||||
}
|
||||
|
||||
glm::vec4 rect_from_bounds_get(glm::vec4 bounds)
|
||||
{
|
||||
return {bounds.x, bounds.y, bounds.z - bounds.x, bounds.w - bounds.y};
|
||||
}
|
||||
}
|
||||
|
||||
int AreaManager::index_get(Entity& character)
|
||||
{
|
||||
auto& data = character.data;
|
||||
@@ -32,14 +54,61 @@ namespace game::state::play
|
||||
return &character.data.areaSchema.areas[index];
|
||||
}
|
||||
|
||||
glm::vec4 AreaManager::bounds_get(Entity& character, const glm::vec4& fallback)
|
||||
{
|
||||
auto index = index_get(character);
|
||||
if (index == -1 || index >= (int)bounds.size()) return fallback;
|
||||
auto result = bounds[index];
|
||||
return rect_is_valid(rect_from_bounds_get(result)) ? result : fallback;
|
||||
}
|
||||
|
||||
glm::vec4 AreaManager::item_spawn_rect_get(Entity& character, const glm::vec4& fallbackBounds)
|
||||
{
|
||||
auto index = index_get(character);
|
||||
if (index == -1 || index >= (int)itemSpawnRects.size()) return rect_from_bounds_get(fallbackBounds);
|
||||
auto result = itemSpawnRects[index];
|
||||
return rect_is_valid(result) ? result : rect_from_bounds_get(bounds_get(character, fallbackBounds));
|
||||
}
|
||||
|
||||
void AreaManager::debug_flags_set(bool isShowLayers, bool isShowNulls)
|
||||
{
|
||||
this->isShowLayers = isShowLayers;
|
||||
this->isShowNulls = isShowNulls;
|
||||
for (auto& entity : entities)
|
||||
{
|
||||
entity.isShowLayers = isShowLayers;
|
||||
entity.isShowNulls = isShowNulls;
|
||||
}
|
||||
}
|
||||
|
||||
void AreaManager::set(Entity& character)
|
||||
{
|
||||
entities.clear();
|
||||
bounds.clear();
|
||||
itemSpawnRects.clear();
|
||||
auto& areas = character.data.areaSchema.areas;
|
||||
entities.reserve(areas.size());
|
||||
bounds.reserve(areas.size());
|
||||
itemSpawnRects.reserve(areas.size());
|
||||
auto* root = character.data.areaSchema.root();
|
||||
auto worldBoundsNull = root && !root->worldBoundsNull.empty() ? root->worldBoundsNull : DEFAULT_WORLD_BOUNDS_NULL;
|
||||
auto itemSpawnNull = root && !root->itemSpawnNull.empty() ? root->itemSpawnNull : worldBoundsNull;
|
||||
|
||||
for (auto& area : areas)
|
||||
entities.emplace_back(area.anm2.is_valid() ? Entity(area.anm2) : Entity{});
|
||||
{
|
||||
auto entity = area.anm2.is_valid() ? Entity(area.anm2) : Entity{};
|
||||
entity.position = {};
|
||||
entity.isShowLayers = isShowLayers;
|
||||
entity.isShowNulls = isShowNulls;
|
||||
auto worldBoundsNullID = entity.nullMap.contains(worldBoundsNull) ? entity.nullMap.at(worldBoundsNull) : -1;
|
||||
auto itemSpawnNullID = entity.nullMap.contains(itemSpawnNull) ? entity.nullMap.at(itemSpawnNull) : worldBoundsNullID;
|
||||
auto rect = entity.null_frame_rect(worldBoundsNullID, entity.defaultAnimationID, ZERO_FLOAT);
|
||||
auto itemSpawnRect = entity.null_frame_rect(itemSpawnNullID, entity.defaultAnimationID, ZERO_FLOAT);
|
||||
if (!rect_is_valid(itemSpawnRect)) itemSpawnRect = rect;
|
||||
entities.emplace_back(std::move(entity));
|
||||
bounds.emplace_back(rect_is_valid(rect) ? bounds_from_rect_get(rect) : glm::vec4(NAN));
|
||||
itemSpawnRects.emplace_back(rect_is_valid(itemSpawnRect) ? itemSpawnRect : glm::vec4(NAN));
|
||||
}
|
||||
}
|
||||
|
||||
void AreaManager::update(Entity& character)
|
||||
@@ -48,6 +117,7 @@ namespace game::state::play
|
||||
|
||||
auto index = index_get(character);
|
||||
if (index == -1 || index >= (int)entities.size()) return;
|
||||
entities[index].position = {};
|
||||
entities[index].update();
|
||||
}
|
||||
|
||||
@@ -56,18 +126,9 @@ namespace game::state::play
|
||||
if (entities.size() != character.data.areaSchema.areas.size()) set(character);
|
||||
|
||||
auto index = index_get(character);
|
||||
if (index == -1) return;
|
||||
if (index == -1 || index >= (int)entities.size()) return;
|
||||
|
||||
auto& area = character.data.areaSchema.areas[index];
|
||||
if (index < (int)entities.size() && area.anm2.is_valid())
|
||||
{
|
||||
entities[index].render(textureShader, rectShader, canvas);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!area.texture.is_valid()) return;
|
||||
|
||||
auto worldModel = math::quad_model_get(area.texture.size);
|
||||
canvas.texture_render(textureShader, area.texture.id, worldModel);
|
||||
entities[index].position = {};
|
||||
entities[index].render(textureShader, rectShader, canvas);
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,16 @@ namespace game::state::play
|
||||
{
|
||||
public:
|
||||
std::vector<Entity> entities{};
|
||||
std::vector<glm::vec4> bounds{};
|
||||
std::vector<glm::vec4> itemSpawnRects{};
|
||||
bool isShowLayers{};
|
||||
bool isShowNulls{};
|
||||
|
||||
int index_get(Entity&);
|
||||
game::resource::xml::Schema::AreaEntry* get(Entity&);
|
||||
glm::vec4 bounds_get(Entity&, const glm::vec4& fallback);
|
||||
glm::vec4 item_spawn_rect_get(Entity&, const glm::vec4& fallbackBounds);
|
||||
void debug_flags_set(bool isShowLayers, bool isShowNulls);
|
||||
void set(Entity&);
|
||||
void update(Entity&);
|
||||
void render(Entity&, game::resource::Shader&, game::resource::Shader&, Canvas&);
|
||||
|
||||
+78
-118
@@ -19,6 +19,7 @@ namespace game::state::play
|
||||
constexpr float CURSOR_SPEED_SCALE_MIN = 0.5f;
|
||||
constexpr float CURSOR_SPEED_SCALE_MAX = 2.0f;
|
||||
constexpr float CALORIE_EPSILON = 0.001f;
|
||||
constexpr float FOOD_TIE_PICK_WEIGHT = 1.0f;
|
||||
|
||||
glm::vec2 rect_center(glm::vec4 rect) { return {rect.x + rect.z * 0.5f, rect.y + rect.w * 0.5f}; }
|
||||
|
||||
@@ -62,13 +63,12 @@ namespace game::state::play
|
||||
bool is_food(const Schema& schema, const Schema::ItemEntry& item)
|
||||
{
|
||||
return item.categoryID >= 0 && item.categoryID < (int)schema.categories.size() &&
|
||||
schema.categories[item.categoryID].isEdible;
|
||||
schema.categories[item.categoryID].useMode == "Edible";
|
||||
}
|
||||
|
||||
bool is_negative_effect(const Schema::ItemEntry& item)
|
||||
{
|
||||
return (item.isCapacityBonus && item.capacityBonus < 0.0f) ||
|
||||
(item.isEatSpeedBonus && item.eatSpeedBonus < 0.0f) ||
|
||||
(item.isDigestionBonus && item.digestionBonus < 0.0f);
|
||||
}
|
||||
|
||||
@@ -77,45 +77,24 @@ namespace game::state::play
|
||||
return is_food(schema, item) && !is_negative_effect(item);
|
||||
}
|
||||
|
||||
int durability_max_get(const Schema& schema, const Schema::ItemEntry& item)
|
||||
{
|
||||
return item.isDurability ? item.durability : schema.durability;
|
||||
}
|
||||
|
||||
float calories_per_bite_get(const Schema& schema, const Schema::ItemEntry& item)
|
||||
{
|
||||
auto durabilityMax = durability_max_get(schema, item);
|
||||
return item.isCalories && durabilityMax > 0 ? item.calories / (float)durabilityMax : 0.0f;
|
||||
}
|
||||
|
||||
bool can_eat(const Entity& character, const Schema& schema, const Schema::ItemEntry& item)
|
||||
{
|
||||
auto caloriesPerBite = calories_per_bite_get(schema, item);
|
||||
return caloriesPerBite > 0.0f && character.calories + caloriesPerBite <= character.max_capacity();
|
||||
return is_autofeed_food(schema, item) && item.isCalories && item.calories > 0.0f &&
|
||||
character.calories + item.calories <= character.max_capacity();
|
||||
}
|
||||
|
||||
struct FoodScore
|
||||
{
|
||||
float calories{};
|
||||
int bites{};
|
||||
float caloriesPerBite{};
|
||||
float value{};
|
||||
bool isValid{};
|
||||
};
|
||||
|
||||
FoodScore food_score_get(const Entity& character, const Schema& schema, const Schema::ItemEntry& item,
|
||||
int durability)
|
||||
FoodScore food_score_get(const Entity& character, const Schema& schema, const Schema::ItemEntry& item)
|
||||
{
|
||||
if (!is_autofeed_food(schema, item)) return {};
|
||||
|
||||
auto caloriesPerBite = calories_per_bite_get(schema, item);
|
||||
auto remainingCapacity = character.max_capacity() - character.calories;
|
||||
auto durabilityMax = durability_max_get(schema, item);
|
||||
auto remainingBites = glm::max(0, durabilityMax - durability);
|
||||
if (caloriesPerBite <= 0.0f || remainingCapacity < caloriesPerBite || remainingBites <= 0) return {};
|
||||
|
||||
auto bites = glm::clamp((int)std::floor(remainingCapacity / caloriesPerBite), 0, remainingBites);
|
||||
auto calories = (float)bites * caloriesPerBite;
|
||||
return {.calories = calories, .bites = bites, .caloriesPerBite = caloriesPerBite, .isValid = bites > 0};
|
||||
if (!can_eat(character, schema, item)) return {};
|
||||
return {.calories = item.calories, .bites = 1, .value = item.calories, .isValid = true};
|
||||
}
|
||||
|
||||
bool is_better_food_score(const FoodScore& score, const FoodScore& best)
|
||||
@@ -124,10 +103,31 @@ namespace game::state::play
|
||||
if (!best.isValid) return true;
|
||||
if (score.calories != best.calories) return score.calories > best.calories;
|
||||
if (score.bites != best.bites) return score.bites < best.bites;
|
||||
return score.caloriesPerBite > best.caloriesPerBite;
|
||||
return score.value > best.value;
|
||||
}
|
||||
|
||||
bool can_eat_anything(Entity& character, ItemManager& itemManager, menu::Inventory& inventory)
|
||||
bool is_same_food_score(const FoodScore& score, const FoodScore& best)
|
||||
{
|
||||
return score.isValid && best.isValid && score.calories == best.calories && score.bites == best.bites &&
|
||||
score.value == best.value;
|
||||
}
|
||||
|
||||
bool food_score_select(FoodScore& best, int& count, const FoodScore& score)
|
||||
{
|
||||
if (is_better_food_score(score, best))
|
||||
{
|
||||
best = score;
|
||||
count = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!is_same_food_score(score, best)) return false;
|
||||
|
||||
count++;
|
||||
return math::random_max((float)count) < FOOD_TIE_PICK_WEIGHT;
|
||||
}
|
||||
|
||||
bool can_eat_anything(Entity& character, ItemManager& itemManager)
|
||||
{
|
||||
auto& schema = character.data.itemSchema;
|
||||
for (auto& entity : itemManager.items)
|
||||
@@ -137,47 +137,6 @@ namespace game::state::play
|
||||
if (is_autofeed_food(schema, item) && can_eat(character, schema, item)) return true;
|
||||
}
|
||||
|
||||
for (auto& [id, quantity] : inventory.values)
|
||||
{
|
||||
if (quantity <= 0 || id < 0 || id >= (int)schema.items.size()) continue;
|
||||
auto& item = schema.items[id];
|
||||
if (is_autofeed_food(schema, item) && can_eat(character, schema, item)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
int best_inventory_food_get(Entity& character, menu::Inventory& inventory)
|
||||
{
|
||||
auto& schema = character.data.itemSchema;
|
||||
auto bestID = -1;
|
||||
auto bestScore = FoodScore{};
|
||||
|
||||
for (auto& [id, quantity] : inventory.values)
|
||||
{
|
||||
if (quantity <= 0 || id < 0 || id >= (int)schema.items.size()) continue;
|
||||
auto& item = schema.items[id];
|
||||
auto score = food_score_get(character, schema, item, 0);
|
||||
|
||||
if (is_better_food_score(score, bestScore))
|
||||
{
|
||||
bestScore = score;
|
||||
bestID = id;
|
||||
}
|
||||
}
|
||||
|
||||
return bestID;
|
||||
}
|
||||
|
||||
bool is_inventory_food(Entity& character, menu::Inventory& inventory)
|
||||
{
|
||||
auto& schema = character.data.itemSchema;
|
||||
for (auto& [id, quantity] : inventory.values)
|
||||
{
|
||||
if (quantity <= 0 || id < 0 || id >= (int)schema.items.size()) continue;
|
||||
if (is_autofeed_food(schema, schema.items[id])) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -193,10 +152,9 @@ namespace game::state::play
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_food(Entity& character, menu::Inventory& inventory, ItemManager& itemManager)
|
||||
bool is_food(Entity& character, ItemManager& itemManager)
|
||||
{
|
||||
return character.calories > CALORIE_EPSILON || is_inventory_food(character, inventory) ||
|
||||
is_world_food(character, itemManager);
|
||||
return character.calories > CALORIE_EPSILON || is_world_food(character, itemManager);
|
||||
}
|
||||
|
||||
int best_world_food_get(Entity& character, ItemManager& itemManager, bool requireCanEat)
|
||||
@@ -204,6 +162,7 @@ namespace game::state::play
|
||||
auto& schema = character.data.itemSchema;
|
||||
auto bestIndex = -1;
|
||||
auto bestScore = FoodScore{};
|
||||
auto bestCount = 0;
|
||||
|
||||
for (int i = 0; i < (int)itemManager.items.size(); i++)
|
||||
{
|
||||
@@ -214,17 +173,25 @@ namespace game::state::play
|
||||
if (!is_autofeed_food(schema, item)) continue;
|
||||
if (requireCanEat && !can_eat(character, schema, item)) continue;
|
||||
|
||||
auto score = food_score_get(character, schema, item, entity.durability);
|
||||
if (is_better_food_score(score, bestScore))
|
||||
{
|
||||
bestScore = score;
|
||||
bestIndex = i;
|
||||
}
|
||||
auto score = food_score_get(character, schema, item);
|
||||
if (food_score_select(bestScore, bestCount, score)) bestIndex = i;
|
||||
}
|
||||
|
||||
return bestIndex;
|
||||
}
|
||||
|
||||
bool world_food_target_is_valid(Entity& character, ItemManager& itemManager, int index)
|
||||
{
|
||||
auto& schema = character.data.itemSchema;
|
||||
if (index < 0 || index >= (int)itemManager.items.size()) return false;
|
||||
|
||||
auto& entity = itemManager.items[index];
|
||||
if (entity.schemaID < 0 || entity.schemaID >= (int)schema.items.size()) return false;
|
||||
|
||||
auto& item = schema.items[entity.schemaID];
|
||||
return can_eat(character, schema, item) && is_finite(entity.rect());
|
||||
}
|
||||
|
||||
bool eat_rect_get(Entity& character, glm::vec4& rect)
|
||||
{
|
||||
for (auto* eatArea : character.data.eat_areas_get())
|
||||
@@ -257,16 +224,18 @@ namespace game::state::play
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void state_reset(Entity& cursor, bool& isItemMouseHeld, bool& isDigestMouseHeld, int& digestClickCooldown)
|
||||
void state_reset(Entity& cursor, bool& isItemMouseHeld, bool& isDigestMouseHeld, int& targetItemIndex,
|
||||
int& digestClickCooldown)
|
||||
{
|
||||
isItemMouseHeld = false;
|
||||
isDigestMouseHeld = false;
|
||||
targetItemIndex = -1;
|
||||
digestClickCooldown = 0;
|
||||
if (!is_finite(cursor.position)) cursor.position = {};
|
||||
}
|
||||
}
|
||||
|
||||
void Autofeed::update(Resources& resources, Entity& character, Entity& cursor, menu::Inventory& inventory,
|
||||
void Autofeed::update(Resources& resources, Entity& character, Entity& cursor, menu::Inventory&,
|
||||
ItemManager& itemManager, CharacterManager& characterManager, Canvas& canvas)
|
||||
{
|
||||
isActive = false;
|
||||
@@ -274,30 +243,27 @@ namespace game::state::play
|
||||
|
||||
if (!isEnabled)
|
||||
{
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_food(character, inventory, itemManager))
|
||||
{
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, targetItemIndex, digestClickCooldown);
|
||||
return;
|
||||
}
|
||||
|
||||
if (character.isStageUp || character.isJustStageUp || character.isJustStageFinal)
|
||||
{
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, targetItemIndex, digestClickCooldown);
|
||||
return;
|
||||
}
|
||||
|
||||
itemManager.spawn_queue(character);
|
||||
|
||||
if (!is_food(character, itemManager))
|
||||
{
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, targetItemIndex, digestClickCooldown);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cursor.position == glm::vec2{} && is_finite(character.position)) cursor.position = character.position;
|
||||
|
||||
auto& schema = character.data.itemSchema;
|
||||
if (inventory.upgrade_all_possible(schema))
|
||||
{
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
|
||||
return;
|
||||
}
|
||||
|
||||
auto itemInput = ItemManager::Input{};
|
||||
itemInput.isAutomation = true;
|
||||
@@ -315,7 +281,7 @@ namespace game::state::play
|
||||
{
|
||||
isActive = false;
|
||||
cursor.isVisible = false;
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, targetItemIndex, digestClickCooldown);
|
||||
return;
|
||||
}
|
||||
auto& schemaItem = schema.items[heldItem.schemaID];
|
||||
@@ -338,10 +304,14 @@ namespace game::state::play
|
||||
isActive = true;
|
||||
cursor.isVisible = true;
|
||||
isItemMouseHeld = false;
|
||||
targetItemIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
auto bestWorldFood = isEatTarget ? best_world_food_get(character, itemManager, true) : -1;
|
||||
if (!isEatTarget || !world_food_target_is_valid(character, itemManager, targetItemIndex))
|
||||
targetItemIndex = isEatTarget ? best_world_food_get(character, itemManager, true) : -1;
|
||||
|
||||
auto bestWorldFood = targetItemIndex;
|
||||
|
||||
if (bestWorldFood != -1)
|
||||
{
|
||||
@@ -349,7 +319,7 @@ namespace game::state::play
|
||||
auto rect = item.rect();
|
||||
if (!is_finite(rect))
|
||||
{
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, targetItemIndex, digestClickCooldown);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -359,6 +329,7 @@ namespace game::state::play
|
||||
|
||||
auto isInItemRect = math::is_point_in_rectf(rect, cursor.position);
|
||||
|
||||
itemInput.targetItemIndex = bestWorldFood;
|
||||
itemInput.isMouseLeftClicked = isInItemRect && !isItemMouseHeld;
|
||||
itemInput.isMouseLeftDown = isInItemRect || isItemMouseHeld;
|
||||
itemManager.inputOverride = itemInput;
|
||||
@@ -367,37 +338,26 @@ namespace game::state::play
|
||||
return;
|
||||
}
|
||||
|
||||
auto isInventoryFood = is_inventory_food(character, inventory);
|
||||
auto isWorldFood = is_world_food(character, itemManager);
|
||||
if (character.calories <= CALORIE_EPSILON && !isInventoryFood && !isWorldFood)
|
||||
if (character.calories <= CALORIE_EPSILON && !isWorldFood)
|
||||
{
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, targetItemIndex, digestClickCooldown);
|
||||
return;
|
||||
}
|
||||
|
||||
auto bestInventoryFood = isEatTarget ? best_inventory_food_get(character, inventory) : -1;
|
||||
if (bestInventoryFood != -1 && !isWorldFood && (int)itemManager.items.size() < ItemManager::LIMIT)
|
||||
{
|
||||
itemManager.queuedItemIDs.emplace_back(bestInventoryFood);
|
||||
inventory.values[bestInventoryFood]--;
|
||||
schema.root()->soundSummon.play();
|
||||
cursor.isVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if ((isWorldFood && bestWorldFood == -1) || !can_eat_anything(character, itemManager, inventory))
|
||||
if ((isWorldFood && bestWorldFood == -1) || !can_eat_anything(character, itemManager))
|
||||
{
|
||||
auto* interactArea = digestion_area_get(character);
|
||||
if (!interactArea)
|
||||
{
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, targetItemIndex, digestClickCooldown);
|
||||
return;
|
||||
}
|
||||
|
||||
auto rect = character.null_frame_rect(character.data.null_id_get(interactArea->null));
|
||||
if (!is_finite(rect))
|
||||
{
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, targetItemIndex, digestClickCooldown);
|
||||
return;
|
||||
}
|
||||
auto radius = glm::max(1.0f, glm::min(rect.z, rect.w) * 0.25f);
|
||||
@@ -434,14 +394,14 @@ namespace game::state::play
|
||||
return;
|
||||
}
|
||||
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, digestClickCooldown);
|
||||
state_reset(cursor, isItemMouseHeld, isDigestMouseHeld, targetItemIndex, digestClickCooldown);
|
||||
}
|
||||
|
||||
void Autofeed::toggle() { isEnabled = !isEnabled; }
|
||||
void Autofeed::disable() { isEnabled = false; }
|
||||
bool Autofeed::available_get(Entity& character, menu::Inventory& inventory, ItemManager& itemManager) const
|
||||
bool Autofeed::available_get(Entity& character, ItemManager& itemManager) const
|
||||
{
|
||||
return is_food(character, inventory, itemManager);
|
||||
return is_food(character, itemManager) || itemManager.spawn_possible_get(character);
|
||||
}
|
||||
bool Autofeed::enabled_get() const { return isEnabled; }
|
||||
bool Autofeed::active_get() const { return isActive; }
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace game::state::play
|
||||
{
|
||||
bool isItemMouseHeld{};
|
||||
bool isDigestMouseHeld{};
|
||||
int targetItemIndex{-1};
|
||||
int digestClickCooldown{};
|
||||
float digestAngle{};
|
||||
bool isEnabled{};
|
||||
@@ -21,7 +22,7 @@ namespace game::state::play
|
||||
CharacterManager&, Canvas&);
|
||||
void toggle();
|
||||
void disable();
|
||||
bool available_get(Entity& character, menu::Inventory&, ItemManager&) const;
|
||||
bool available_get(Entity& character, ItemManager&) const;
|
||||
bool enabled_get() const;
|
||||
bool active_get() const;
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
#include "../../util/math.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <algorithm>
|
||||
#include <imgui.h>
|
||||
#include <optional>
|
||||
|
||||
using namespace game::resource::xml;
|
||||
@@ -139,10 +139,10 @@ namespace game::state::play
|
||||
|
||||
if (layerID != -1)
|
||||
{
|
||||
auto scaleEffectTimeTicks = interactArea->timeTicks == ZERO_FLOAT ? SCALE_EFFECT_TIME_TICKS_DEFAULT
|
||||
: interactArea->timeTicks;
|
||||
auto scaleEffectTimeTicks =
|
||||
interactArea->timeTicks == ZERO_FLOAT ? SCALE_EFFECT_TIME_TICKS_DEFAULT : interactArea->timeTicks;
|
||||
auto scaleEffectCycles = interactArea->scaleEffectCycles == ZERO_FLOAT ? SCALE_EFFECT_CYCLES_DEFAULT
|
||||
: interactArea->scaleEffectCycles;
|
||||
: interactArea->scaleEffectCycles;
|
||||
character.overrides.emplace_back(Entity::Override(
|
||||
layerID, Anm2::LAYER, Entity::Override::ADD, {.scale = glm::vec2(interactArea->scaleEffectAmplitude)},
|
||||
std::optional<float>(scaleEffectTimeTicks), interact_area_override_tick, scaleEffectCycles));
|
||||
@@ -204,10 +204,8 @@ namespace game::state::play
|
||||
cursorWorldPositionPrevious = cursorWorldPosition;
|
||||
|
||||
if (character.isJustDigested && text.is_interruptible())
|
||||
if (auto* digest = dialogue.get(Schema::Element::DIGEST))
|
||||
text.set(dialogue.dialogue_pool_entry_get(*digest), character);
|
||||
text.set(dialogue.dialogue_hook_entry_get(Schema::Element::DIGEST), character);
|
||||
if (character.isJustStageUp)
|
||||
if (auto* stageUp = dialogue.get(Schema::Element::STAGE_UP))
|
||||
text.set(dialogue.dialogue_pool_entry_get(*stageUp), character);
|
||||
text.set(dialogue.dialogue_hook_entry_get(Schema::Element::STAGE_UP, character.stage_get() + 1), character);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
|
||||
#include "../../util/imgui/input_int_ex.hpp"
|
||||
#include "../../util/imgui/widget.hpp"
|
||||
|
||||
using namespace game::util::imgui;
|
||||
@@ -12,7 +11,7 @@ using namespace game::resource::xml;
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
void Cheats::update(Resources&, Entity& character, menu::Inventory& inventory)
|
||||
void Cheats::update(Resources&, Entity& character)
|
||||
{
|
||||
auto& strings = character.data.strings;
|
||||
|
||||
@@ -27,8 +26,6 @@ namespace game::state::play
|
||||
auto digestionRateMin = (float)character.data.root()->digestionRateMin * Entity::UPDATE_RATE;
|
||||
auto digestionRateMax = (float)character.data.root()->digestionRateMax * Entity::UPDATE_RATE;
|
||||
auto digestionRate = character.digestion_rate_get();
|
||||
auto eatSpeedMin = (float)character.data.root()->eatSpeedMinMultiplier;
|
||||
auto eatSpeedMax = (float)character.data.root()->eatSpeedMaxMultiplier;
|
||||
|
||||
auto weight_update = [&]()
|
||||
{
|
||||
@@ -59,31 +56,10 @@ namespace game::state::play
|
||||
digestionRateMin, digestionRateMax,
|
||||
strings.get(Strings::CheatsDigestionRateFormat).c_str())))
|
||||
character.digestionRate = digestionRate / Entity::UPDATE_RATE;
|
||||
WIDGET_FX(ImGui::SliderFloat(strings.get(Strings::CheatsEatSpeed).c_str(), &character.eatSpeed, eatSpeedMin,
|
||||
eatSpeedMax,
|
||||
strings.get(Strings::CheatsEatSpeedFormat).c_str()));
|
||||
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::CheatsDigestButton).c_str())))
|
||||
character.digestionProgress = Entity::DIGESTION_MAX;
|
||||
|
||||
ImGui::SeparatorText(strings.get(Strings::CheatsInventory).c_str());
|
||||
|
||||
if (ImGui::BeginChild("##Inventory", ImGui::GetContentRegionAvail(), ImGuiChildFlags_Borders))
|
||||
{
|
||||
auto& schema = character.data.itemSchema;
|
||||
|
||||
ImGui::PushItemWidth(100);
|
||||
for (int i = 0; i < (int)schema.items.size(); i++)
|
||||
{
|
||||
auto& item = schema.items[i];
|
||||
ImGui::PushID(i);
|
||||
WIDGET_FX(input_int_range(item.name.c_str(), &inventory.values[i], 0, schema.quantityMax, 1, 5));
|
||||
ImGui::SetItemTooltip("%s", item.name.c_str());
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::PopItemWidth();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "menu/inventory.hpp"
|
||||
#include "text.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
@@ -10,6 +9,6 @@ namespace game::state::play
|
||||
class Cheats
|
||||
{
|
||||
public:
|
||||
void update(Resources&, Entity&, menu::Inventory&);
|
||||
void update(Resources&, Entity&);
|
||||
};
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
#include "info.hpp"
|
||||
|
||||
#include "../../util/color.hpp"
|
||||
#include "../../util/imgui.hpp"
|
||||
#include "../../util/math.hpp"
|
||||
#include "../../util/string.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
|
||||
using namespace game::resource;
|
||||
using namespace game::resource::xml;
|
||||
using namespace game::util;
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
float info_height_get()
|
||||
{
|
||||
static constexpr auto HEIGHT_MULTIPLIER = 4.0f;
|
||||
return ImGui::GetTextLineHeightWithSpacing() * HEIGHT_MULTIPLIER;
|
||||
}
|
||||
|
||||
void info_content_draw(Resources& resources, Entity& character, ImVec2 size)
|
||||
{
|
||||
static constexpr auto HALF_MULTIPLIER = 0.5f;
|
||||
static constexpr auto ZERO_FLOAT = 0.0f;
|
||||
|
||||
auto& strings = character.data.strings;
|
||||
auto& style = ImGui::GetStyle();
|
||||
auto childSize = ImVec2((size.x - style.ItemSpacing.x) * HALF_MULTIPLIER, size.y);
|
||||
|
||||
if (ImGui::BeginChild("##Weight", childSize))
|
||||
{
|
||||
auto* settings = resources.settings.root();
|
||||
auto system = settings->measurementSystem == "Imperial" ? measurement::IMPERIAL : measurement::METRIC;
|
||||
auto weight = character.weight_get(system);
|
||||
auto stage = character.stage_get();
|
||||
auto stageMax = character.stage_max_get();
|
||||
auto stageCount = character.stage_count_get();
|
||||
auto stageWeight = character.stage_threshold_get(stage, system);
|
||||
auto stageNextWeight = character.stage_threshold_next_get(system);
|
||||
auto unitString = (system == measurement::IMPERIAL ? "lbs" : "kg");
|
||||
|
||||
auto weightString = util::string::format_commas(weight, 2) + " " + unitString;
|
||||
imgui::text_unformatted_fit_draw(weightString.c_str(), ImVec2(ImGui::GetContentRegionAvail().x, Font::HEADER_2),
|
||||
Font::HEADER_2);
|
||||
ImGui::SetItemTooltip("%s", weightString.c_str());
|
||||
|
||||
auto stageProgress = character.stage_progress_get();
|
||||
ImGui::ProgressBar(stageProgress, ImVec2(ImGui::GetContentRegionAvail().x, ZERO_FLOAT),
|
||||
strings.get(stage >= stageMax ? Strings::InfoProgressMax : Strings::InfoProgressToNextStage)
|
||||
.c_str());
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::Text(strings.get(Strings::InfoStageProgressFormat).c_str(), stage + 1, stageCount,
|
||||
math::to_percent(stageProgress));
|
||||
ImGui::Separator();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, imgui::to_imvec4(color::GRAY));
|
||||
if (stage >= stageMax)
|
||||
ImGui::TextUnformatted(strings.get(Strings::InfoMaxedOut).c_str());
|
||||
else
|
||||
{
|
||||
ImGui::Text(strings.get(Strings::InfoStageStartFormat).c_str(), stageWeight, unitString);
|
||||
ImGui::Text(strings.get(Strings::InfoStageCurrentFormat).c_str(), weight, unitString);
|
||||
ImGui::Text(strings.get(Strings::InfoStageNextFormat).c_str(), stageNextWeight, unitString);
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::BeginChild("##Calories and Capacity", childSize))
|
||||
{
|
||||
auto& calories = character.calories;
|
||||
auto& capacity = character.capacity;
|
||||
|
||||
auto overstuffedPercent = std::max(ZERO_FLOAT, (calories - capacity) / (character.max_capacity() - capacity));
|
||||
auto caloriesColor = ImVec4(1.0f, 1.0f - overstuffedPercent, 1.0f - overstuffedPercent, 1.0f);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, caloriesColor);
|
||||
auto caloriesString = std::format("{:.0f} kcal / {:.0f} kcal", calories,
|
||||
character.is_over_capacity() ? character.max_capacity() : character.capacity);
|
||||
imgui::text_unformatted_fit_draw(caloriesString.c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x, Font::HEADER_2), Font::HEADER_2);
|
||||
ImGui::SetItemTooltip("%s", caloriesString.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
auto digestionProgress = character.isDigesting
|
||||
? (float)character.digestionTimer / character.data.root()->digestionTimerMax
|
||||
: character.digestionProgress / Entity::DIGESTION_MAX;
|
||||
ImGui::ProgressBar(digestionProgress, ImVec2(ImGui::GetContentRegionAvail().x, ZERO_FLOAT),
|
||||
strings.get(character.isDigesting ? Strings::InfoDigesting : Strings::InfoDigestion).c_str());
|
||||
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
if (character.isDigesting)
|
||||
ImGui::TextUnformatted(strings.get(Strings::InfoDigestionInProgress).c_str());
|
||||
else if (digestionProgress <= ZERO_FLOAT)
|
||||
ImGui::TextUnformatted(strings.get(Strings::InfoGiveFoodToStartDigesting).c_str());
|
||||
else
|
||||
ImGui::Text("%0.2f%%", math::to_percent(digestionProgress));
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY)));
|
||||
|
||||
ImGui::Text(strings.get(Strings::InfoDigestionRateFormat).c_str(), character.digestion_rate_get());
|
||||
ImGui::Text(strings.get(Strings::InfoEatingSpeedFormat).c_str(), character.eatSpeed);
|
||||
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
void Info::update(Resources& resources, Entity& character, float fadeAlpha)
|
||||
{
|
||||
static constexpr auto WIDTH_MULTIPLIER = 0.30f;
|
||||
static constexpr auto ALPHA_MIN = 0.0f;
|
||||
static constexpr auto ALPHA_MAX = 1.0f;
|
||||
static constexpr auto STYLE_VAR_COUNT = 1;
|
||||
if (fadeAlpha <= ALPHA_MIN) return;
|
||||
|
||||
auto& style = ImGui::GetStyle();
|
||||
auto windowSize = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
|
||||
|
||||
auto size = ImVec2(windowSize.x * WIDTH_MULTIPLIER - (style.WindowPadding.x * 2.0f),
|
||||
info_height_get());
|
||||
auto pos = ImVec2((windowSize.x * 0.5f) - (size.x * 0.5f), style.WindowPadding.y);
|
||||
|
||||
ImGui::SetNextWindowSize(size);
|
||||
ImGui::SetNextWindowPos(pos);
|
||||
|
||||
auto flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove;
|
||||
if (fadeAlpha < ALPHA_MAX) flags |= ImGuiWindowFlags_NoInputs;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, style.Alpha * fadeAlpha);
|
||||
if (ImGui::Begin("##Info", nullptr, flags))
|
||||
info_content_draw(resources, character, ImGui::GetContentRegionAvail());
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(STYLE_VAR_COUNT);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../entity.hpp"
|
||||
#include "../../resources.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
float info_height_get();
|
||||
void info_content_draw(Resources&, Entity&, ImVec2);
|
||||
|
||||
class Info
|
||||
{
|
||||
public:
|
||||
void update(Resources&, Entity&, float fadeAlpha);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "item_display.hpp"
|
||||
|
||||
#include "../../resource/font.hpp"
|
||||
#include "../../util/color.hpp"
|
||||
#include "../../util/imgui.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
constexpr auto ZERO_FLOAT = 0.0f;
|
||||
|
||||
void item_header_draw(const resource::xml::Schema::ItemEntry& item)
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_2);
|
||||
ImGui::TextWrapped("%s", item.name.c_str());
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
void item_summary_draw(Entity& character, resource::xml::Schema& schema, const resource::xml::Schema::ItemEntry& item)
|
||||
{
|
||||
using Strings = resource::xml::Strings;
|
||||
|
||||
auto& strings = character.data.strings;
|
||||
auto isCategory = item.categoryID >= 0 && item.categoryID < (int)schema.categories.size();
|
||||
auto isRarity = item.rarityID >= 0 && item.rarityID < (int)schema.rarities.size();
|
||||
auto isFlavor = item.flavorID >= 0 && item.flavorID < (int)schema.flavors.size();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(util::imgui::to_imvec4(util::color::GRAY)));
|
||||
if (isCategory && isRarity)
|
||||
ImGui::TextWrapped("-- %s (%s) --", schema.categories[item.categoryID].name.c_str(),
|
||||
schema.rarities[item.rarityID].name.c_str());
|
||||
else if (isCategory)
|
||||
ImGui::TextWrapped("-- %s --", schema.categories[item.categoryID].name.c_str());
|
||||
if (item.isFlavor && isFlavor)
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryFlavorFormat).c_str(), schema.flavors[item.flavorID].name.c_str());
|
||||
if (item.isCalories) ImGui::TextWrapped(strings.get(Strings::InventoryCaloriesFormat).c_str(), item.calories);
|
||||
if (item.isCapacityBonus)
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryCapacityBonusFormat).c_str(), item.capacityBonus);
|
||||
if (item.isDigestionBonus)
|
||||
{
|
||||
if (item.digestionBonus > ZERO_FLOAT)
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryDigestionRateBonusFormat).c_str(),
|
||||
item.digestionBonus * Entity::UPDATE_RATE);
|
||||
else if (item.digestionBonus < ZERO_FLOAT)
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryDigestionRatePenaltyFormat).c_str(),
|
||||
item.digestionBonus * Entity::UPDATE_RATE);
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
void item_tooltip_draw(Entity& character, resource::xml::Schema& schema, const resource::xml::Schema::ItemEntry& item)
|
||||
{
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * ITEM_TOOLTIP_WRAP_FONT_MULTIPLIER);
|
||||
item_header_draw(item);
|
||||
ImGui::Separator();
|
||||
item_summary_draw(character, schema, item);
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", item.description.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../entity.hpp"
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
inline constexpr auto ITEM_TOOLTIP_WRAP_FONT_MULTIPLIER = 24.0f;
|
||||
|
||||
void item_header_draw(const resource::xml::Schema::ItemEntry&);
|
||||
void item_summary_draw(Entity&, resource::xml::Schema&, const resource::xml::Schema::ItemEntry&);
|
||||
void item_tooltip_draw(Entity&, resource::xml::Schema&, const resource::xml::Schema::ItemEntry&);
|
||||
}
|
||||
+230
-123
@@ -1,4 +1,5 @@
|
||||
#include "item_manager.hpp"
|
||||
#include "item_display.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
@@ -15,44 +16,100 @@ using namespace glm;
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
namespace
|
||||
static inline float item_spawn_weight_get(resource::xml::Schema& itemSchema, Entity& character, int id)
|
||||
{
|
||||
int durability_animation_index_get(const resource::xml::Schema& schema, const resource::xml::Anm2& anm2,
|
||||
int durability, int durabilityMax)
|
||||
{
|
||||
if (durability >= durabilityMax) return -1;
|
||||
static constexpr auto ZERO_FLOAT = 0.0f;
|
||||
|
||||
auto animationName = schema.root()->animationChew + std::to_string(std::max(0, durability));
|
||||
return anm2.animationMap.contains(animationName) ? anm2.animationMap.at(animationName) : -1;
|
||||
if (id < 0 || id >= (int)itemSchema.items.size()) return ZERO_FLOAT;
|
||||
|
||||
auto& item = itemSchema.items[id];
|
||||
if (item.isCalories && item.calories > character.max_capacity()) return ZERO_FLOAT;
|
||||
if (item.rarityID < 0 || item.rarityID >= (int)itemSchema.rarities.size()) return ZERO_FLOAT;
|
||||
return glm::max(ZERO_FLOAT, itemSchema.rarities[item.rarityID].weight);
|
||||
}
|
||||
|
||||
int ItemManager::spawn_delay_get(Entity& character)
|
||||
{
|
||||
static constexpr auto ZERO_FLOAT = 0.0f;
|
||||
static constexpr auto ONE_INT = 1;
|
||||
static constexpr auto SPAWN_DELAY_TICKS_DEFAULT = 60;
|
||||
static constexpr auto SPAWN_DELAY_STAGE_MULTIPLIER_DEFAULT = 1.0f;
|
||||
|
||||
auto* itemRoot = character.data.itemSchema.root();
|
||||
auto stage = glm::clamp(0, character.stage_get(), character.stage_max_get());
|
||||
auto baseSpawnDelay =
|
||||
itemRoot && itemRoot->baseSpawnDelayTicks > 0 ? itemRoot->baseSpawnDelayTicks : SPAWN_DELAY_TICKS_DEFAULT;
|
||||
auto spawnDelayMultiplier = itemRoot && itemRoot->spawnDelayStageMultiplier > ZERO_FLOAT
|
||||
? itemRoot->spawnDelayStageMultiplier
|
||||
: SPAWN_DELAY_STAGE_MULTIPLIER_DEFAULT;
|
||||
return glm::max(ONE_INT, (int)std::round((float)baseSpawnDelay * std::pow(spawnDelayMultiplier, stage)));
|
||||
}
|
||||
|
||||
bool ItemManager::spawn_possible_get(Entity& character)
|
||||
{
|
||||
static constexpr auto ZERO_FLOAT = 0.0f;
|
||||
|
||||
auto& itemSchema = character.data.itemSchema;
|
||||
auto* itemRoot = itemSchema.root();
|
||||
auto spawnLimit = itemRoot && itemRoot->itemSpawnLimit > 0 ? itemRoot->itemSpawnLimit : LIMIT;
|
||||
if (!itemRoot || (int)items.size() >= spawnLimit || itemSchema.items.empty()) return false;
|
||||
|
||||
for (int i = 0; i < (int)itemSchema.items.size(); i++)
|
||||
if (item_spawn_weight_get(itemSchema, character, i) > ZERO_FLOAT) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ItemManager::spawn_available_get(Entity& character)
|
||||
{
|
||||
return spawnDelayTicks <= 0 && spawn_possible_get(character);
|
||||
}
|
||||
|
||||
bool ItemManager::spawn_queue(Entity& character, int itemID, bool isLimitIgnored)
|
||||
{
|
||||
auto& itemSchema = character.data.itemSchema;
|
||||
auto* itemRoot = itemSchema.root();
|
||||
auto spawnLimit = itemRoot && itemRoot->itemSpawnLimit > 0 ? itemRoot->itemSpawnLimit : LIMIT;
|
||||
if (!itemRoot || itemID < 0 || itemID >= (int)itemSchema.items.size() ||
|
||||
(!isLimitIgnored && (int)items.size() >= spawnLimit))
|
||||
return false;
|
||||
|
||||
auto& item = itemSchema.items[itemID];
|
||||
itemRoot->soundSummon.play();
|
||||
if (item.rarityID >= 0 && item.rarityID < (int)itemSchema.rarities.size())
|
||||
itemSchema.rarities[item.rarityID].sound.play();
|
||||
queuedItemIDs.emplace_back(itemID);
|
||||
spawnDelayTicks = spawn_delay_get(character);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ItemManager::spawn_queue(Entity& character)
|
||||
{
|
||||
static constexpr auto ZERO_FLOAT = 0.0f;
|
||||
|
||||
auto& itemSchema = character.data.itemSchema;
|
||||
if (!spawn_available_get(character)) return false;
|
||||
|
||||
auto total = ZERO_FLOAT;
|
||||
for (int i = 0; i < (int)itemSchema.items.size(); i++)
|
||||
total += item_spawn_weight_get(itemSchema, character, i);
|
||||
|
||||
if (total <= ZERO_FLOAT) return false;
|
||||
|
||||
auto roll = math::random_max(total);
|
||||
auto accumulator = ZERO_FLOAT;
|
||||
auto itemID = -1;
|
||||
for (int i = 0; i < (int)itemSchema.items.size(); i++)
|
||||
{
|
||||
auto weight = item_spawn_weight_get(itemSchema, character, i);
|
||||
if (weight <= ZERO_FLOAT) continue;
|
||||
itemID = i;
|
||||
accumulator += weight;
|
||||
if (roll < accumulator) break;
|
||||
}
|
||||
|
||||
int durability_animation_index_get(const resource::xml::Schema& schema, const Entity& entity, int durability,
|
||||
int durabilityMax)
|
||||
{
|
||||
if (durability >= durabilityMax) return -1;
|
||||
|
||||
auto animationName = schema.root()->animationChew + std::to_string(std::max(0, durability));
|
||||
return entity.animationMap.contains(animationName) ? entity.animationMap.at(animationName) : -1;
|
||||
}
|
||||
|
||||
bool is_finite(glm::vec4 value)
|
||||
{
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z) && std::isfinite(value.w) &&
|
||||
value.z > 0.0f && value.w > 0.0f;
|
||||
}
|
||||
|
||||
glm::vec2 item_spawn_position_get(Entity& character, const glm::vec4& bounds)
|
||||
{
|
||||
auto rect = character.rect();
|
||||
if (!is_finite(rect)) rect = bounds;
|
||||
auto center = glm::vec2(rect.x + rect.z * 0.5f, rect.y + rect.w);
|
||||
auto radius = rect.w * 0.5f;
|
||||
auto angle = glm::pi<float>() + math::random_max(glm::pi<float>());
|
||||
auto position = center + glm::vec2(std::cos(angle), std::sin(angle)) * radius;
|
||||
|
||||
return {glm::clamp(position.x, bounds.x, bounds.x + bounds.z),
|
||||
glm::clamp(position.y, bounds.y, bounds.y + bounds.w)};
|
||||
}
|
||||
if (itemID == -1) return false;
|
||||
return spawn_queue(character, itemID);
|
||||
}
|
||||
|
||||
void ItemManager::update(Entity& character, Entity& cursor, AreaManager& areaManager, Text& text,
|
||||
@@ -74,11 +131,8 @@ namespace game::state::play
|
||||
auto& airResistance = area.airResistance;
|
||||
auto& dialogue = character.data.dialogue;
|
||||
auto isOverCapacity = character.is_over_capacity();
|
||||
auto dialogue_pool_entry_get = [&](resource::xml::Schema::Element::Type type)
|
||||
{
|
||||
auto* reference = dialogue.get(type);
|
||||
return reference ? dialogue.dialogue_pool_entry_get(*reference) : nullptr;
|
||||
};
|
||||
auto dialogue_hook_entry_get = [&](resource::xml::Schema::Element::Type type)
|
||||
{ return dialogue.dialogue_hook_entry_get(type); };
|
||||
auto particles_queue =
|
||||
[&](resource::xml::Schema& particleSchema, resource::xml::Schema::Element::Type type, glm::vec2 position)
|
||||
{
|
||||
@@ -101,8 +155,8 @@ namespace game::state::play
|
||||
auto isMouseLeftDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
|
||||
auto isMouseLeftReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
||||
auto isMouseRightClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Right);
|
||||
auto isMouseRightDown = ImGui::IsMouseDown(ImGuiMouseButton_Right);
|
||||
auto isAutomation = this->isAutomation;
|
||||
auto targetItemIndex = -1;
|
||||
|
||||
auto& io = ImGui::GetIO();
|
||||
|
||||
@@ -113,7 +167,7 @@ namespace game::state::play
|
||||
isMouseLeftDown = inputOverride->isMouseLeftDown;
|
||||
isMouseLeftReleased = inputOverride->isMouseLeftReleased;
|
||||
isMouseRightClicked = inputOverride->isMouseRightClicked;
|
||||
isMouseRightDown = inputOverride->isMouseRightDown;
|
||||
targetItemIndex = inputOverride->targetItemIndex;
|
||||
isAutomation = isAutomation || inputOverride->isAutomation;
|
||||
inputOverride.reset();
|
||||
}
|
||||
@@ -123,6 +177,11 @@ namespace game::state::play
|
||||
if (heldItemIndex != -1)
|
||||
{
|
||||
heldItemIndex = -1;
|
||||
pendingEatItemIndex = -1;
|
||||
pendingEatEventID = -1;
|
||||
pendingEatAnimationIndex = -1;
|
||||
isPendingEatRect = false;
|
||||
isEatTextPending = false;
|
||||
isJustItemHeldStopped = true;
|
||||
}
|
||||
isItemHovered = false;
|
||||
@@ -132,10 +191,20 @@ namespace game::state::play
|
||||
isMouseLeftDown = false;
|
||||
isMouseLeftReleased = false;
|
||||
isMouseRightClicked = false;
|
||||
isMouseRightDown = false;
|
||||
isImguiCaptureMouse = true;
|
||||
}
|
||||
|
||||
auto isEatAnimationFinished = pendingEatAnimationIndex == character.animationIndex &&
|
||||
(character.state == Entity::STOPPED || character.is_animation_finished());
|
||||
if (isEatTextPending && isEatAnimationFinished)
|
||||
{
|
||||
text.set(dialogue_hook_entry_get(isEatTextOverCapacity ? resource::xml::Schema::Element::EAT_FULL
|
||||
: resource::xml::Schema::Element::EAT),
|
||||
character);
|
||||
isEatTextPending = false;
|
||||
pendingEatAnimationIndex = -1;
|
||||
}
|
||||
|
||||
if (isJustItemHoveredStopped)
|
||||
{
|
||||
cursor.play({cursor.defaultAnimation});
|
||||
@@ -155,27 +224,24 @@ namespace game::state::play
|
||||
isItemHovered = false;
|
||||
particles.clear();
|
||||
if (isItemHovered != isItemHoveredPrevious && !isItemHovered) isJustItemHoveredStopped = true;
|
||||
if (spawnDelayTicks > 0) spawnDelayTicks--;
|
||||
if (discoveredItemIDs.size() != schema.items.size()) discoveredItemIDs.resize(schema.items.size());
|
||||
|
||||
for (auto& id : queuedItemIDs)
|
||||
{
|
||||
auto position = item_spawn_position_get(character, bounds);
|
||||
auto spawnRect = areaManager.item_spawn_rect_get(character, bounds);
|
||||
auto position = glm::vec2(spawnRect.x + math::random_max(spawnRect.z),
|
||||
spawnRect.y + math::random_max(spawnRect.w));
|
||||
|
||||
auto& itemSchema = character.data.itemSchema;
|
||||
auto& item = itemSchema.items.at(id);
|
||||
auto& anm2 = itemSchema.anm2s.at(id);
|
||||
auto durabilityMax = item.isDurability ? item.durability : itemSchema.durability;
|
||||
auto animationIndex = durability_animation_index_get(itemSchema, anm2, 0, durabilityMax);
|
||||
items.emplace_back(anm2, position, id, 0, animationIndex);
|
||||
items.emplace_back(anm2, position, id);
|
||||
character.totalItemsSpawned++;
|
||||
if (id >= 0 && id < (int)discoveredItemIDs.size()) discoveredItemIDs[id] = true;
|
||||
particles_queue(itemSchema, resource::xml::Schema::Element::SUMMON, position);
|
||||
}
|
||||
queuedItemIDs.clear();
|
||||
|
||||
if (isMouseRightDown)
|
||||
{
|
||||
if (cursorRoot)
|
||||
if (auto animation = cursorRoot->animationReturn.get()) cursor.play({*animation});
|
||||
}
|
||||
|
||||
if (auto heldItem = vector::find(items, heldItemIndex))
|
||||
{
|
||||
auto& item = schema.items[heldItem->schemaID];
|
||||
@@ -191,24 +257,24 @@ namespace game::state::play
|
||||
rotation *= friction;
|
||||
rotation = glm::clamp(-ROTATION_MAX, rotation, ROTATION_MAX);
|
||||
|
||||
if (schema.categories[item.categoryID].isEdible)
|
||||
auto& category = schema.categories[item.categoryID];
|
||||
if (category.useMode == "Edible")
|
||||
{
|
||||
auto durabilityMax = item.isDurability ? item.durability : schema.durability;
|
||||
auto caloriesPerBite = item.isCalories && durabilityMax > 0 ? item.calories / durabilityMax : 0;
|
||||
auto isCanEat = character.calories + caloriesPerBite <= character.max_capacity();
|
||||
auto calories = item.isCalories ? item.calories : 0.0f;
|
||||
auto isCanEat = calories > 0.0f && character.calories + calories <= character.max_capacity();
|
||||
|
||||
if (isJustItemHeld)
|
||||
{
|
||||
if (!isAutomation)
|
||||
{
|
||||
if (isCanEat)
|
||||
text.set(dialogue_pool_entry_get(isOverCapacity ? resource::xml::Schema::Element::FEED_FULL
|
||||
text.set(dialogue_hook_entry_get(isOverCapacity ? resource::xml::Schema::Element::FEED_FULL
|
||||
: resource::xml::Schema::Element::FEED),
|
||||
character);
|
||||
else if (caloriesPerBite > character.capacity)
|
||||
text.set(dialogue_pool_entry_get(resource::xml::Schema::Element::LOW_CAPACITY), character);
|
||||
else if (calories > character.capacity)
|
||||
text.set(dialogue_hook_entry_get(resource::xml::Schema::Element::LOW_CAPACITY), character);
|
||||
else
|
||||
text.set(dialogue_pool_entry_get(resource::xml::Schema::Element::FULL), character);
|
||||
text.set(dialogue_hook_entry_get(resource::xml::Schema::Element::FULL), character);
|
||||
}
|
||||
isJustItemHeld = false;
|
||||
}
|
||||
@@ -220,77 +286,78 @@ namespace game::state::play
|
||||
|
||||
auto nullID = character.data.null_id_get(eatArea->null);
|
||||
auto rect = character.null_frame_rect(nullID);
|
||||
if (!is_finite(rect))
|
||||
if (!resource::xml::Anm2::is_rect_valid(rect))
|
||||
{
|
||||
auto animationIndex =
|
||||
character.animation_index_get(eatArea->animation + character.animation_append_id_get());
|
||||
rect = character.null_frame_rect(nullID, animationIndex, 0.0f);
|
||||
}
|
||||
|
||||
if (isCanEat && math::is_point_in_rectf(rect, heldItem->position))
|
||||
{
|
||||
character.play({.animation = eatArea->animation,
|
||||
.appendID = character.animation_append_id_get(),
|
||||
.speedMultiplier = character.eatSpeed});
|
||||
auto eventID = eatArea->event.empty() ? eatArea->eventID : character.data.event_id_get(eatArea->event);
|
||||
auto isEatAreaActive = isCanEat && math::is_point_in_rectf(rect, heldItem->position);
|
||||
auto isPendingEatItem = pendingEatItemIndex == heldItemIndex && pendingEatEventID == eventID;
|
||||
auto isPendingEatAreaActive =
|
||||
isPendingEatItem && isPendingEatRect && math::is_point_in_rectf(pendingEatRect, heldItem->position);
|
||||
auto isEatEventActivated = isPendingEatItem && eventID != -1 && character.playedEventID == eventID;
|
||||
|
||||
if (character.playedEventID == character.data.event_id_get(eatArea->event))
|
||||
if (isEatAreaActive || isPendingEatAreaActive)
|
||||
{
|
||||
if (isEatEventActivated)
|
||||
{
|
||||
heldItem->durability++;
|
||||
character.consume_played_event();
|
||||
|
||||
character.calories += caloriesPerBite;
|
||||
character.totalCaloriesConsumed += caloriesPerBite;
|
||||
character.calories += calories;
|
||||
character.totalCaloriesConsumed += calories;
|
||||
|
||||
if (item.isCapacityBonus)
|
||||
{
|
||||
character.capacity += item.capacityBonus / durabilityMax;
|
||||
character.capacity += item.capacityBonus;
|
||||
character.capacity = glm::clamp(character.capacity, (float)character.data.root()->capacityMinCalories,
|
||||
(float)character.data.root()->capacityMaxCalories);
|
||||
}
|
||||
if (item.isEatSpeedBonus)
|
||||
{
|
||||
character.eatSpeed += item.eatSpeedBonus / durabilityMax;
|
||||
character.eatSpeed = glm::clamp(character.eatSpeed, (float)character.data.root()->eatSpeedMinMultiplier,
|
||||
(float)character.data.root()->eatSpeedMaxMultiplier);
|
||||
}
|
||||
if (item.isDigestionBonus)
|
||||
{
|
||||
character.digestionRate += item.digestionBonus / durabilityMax;
|
||||
character.digestionRate += item.digestionBonus;
|
||||
character.digestionRate =
|
||||
glm::clamp(character.digestionRate, (float)character.data.root()->digestionRateMin,
|
||||
(float)character.data.root()->digestionRateMax);
|
||||
}
|
||||
|
||||
if (heldItem->durability >= durabilityMax)
|
||||
character.totalItemsConsumed++;
|
||||
queuedRemoveItemIndex = heldItemIndex;
|
||||
heldItemIndex = -1;
|
||||
pendingEatItemIndex = -1;
|
||||
pendingEatEventID = -1;
|
||||
pendingEatAnimationIndex = character.animationIndex;
|
||||
isPendingEatRect = false;
|
||||
if (!isAutomation)
|
||||
{
|
||||
isQueueFinishFood = true;
|
||||
character.totalFoodItemsEaten++;
|
||||
queuedRemoveItemIndex = heldItemIndex;
|
||||
heldItemIndex = -1;
|
||||
isEatTextPending = true;
|
||||
isEatTextOverCapacity = isOverCapacity;
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
pendingEatItemIndex = heldItemIndex;
|
||||
pendingEatEventID = eventID;
|
||||
if (isEatAreaActive)
|
||||
{
|
||||
auto animationIndex =
|
||||
durability_animation_index_get(schema, *heldItem, heldItem->durability, durabilityMax);
|
||||
heldItem->play({.index = animationIndex, .mode = Entity::SET});
|
||||
pendingEatRect = rect;
|
||||
isPendingEatRect = true;
|
||||
}
|
||||
if (auto animation = character.animation_get(eatArea->animation + character.animation_append_id_get());
|
||||
!animation || !character.is_playing(animation->name))
|
||||
character.play({.animation = eatArea->animation,
|
||||
.appendID = character.animation_append_id_get(),
|
||||
.interrupt = Entity::Interrupt::ALWAYS});
|
||||
}
|
||||
}
|
||||
|
||||
if (isMouseLeftReleased)
|
||||
else if (pendingEatItemIndex == heldItemIndex)
|
||||
{
|
||||
if (fabs(delta.x) >= THROW_THRESHOLD || fabs(delta.y) >= THROW_THRESHOLD)
|
||||
{
|
||||
if (cursorRoot) cursorRoot->soundThrow.play();
|
||||
if (!isAutomation) text.set(dialogue_pool_entry_get(resource::xml::Schema::Element::THROW), character);
|
||||
isJustItemThrown = true;
|
||||
}
|
||||
else if (cursorRoot)
|
||||
cursorRoot->soundRelease.play();
|
||||
|
||||
heldItem->velocity -= delta;
|
||||
heldItemIndex = -1;
|
||||
isJustItemHeldStopped = true;
|
||||
pendingEatItemIndex = -1;
|
||||
pendingEatEventID = -1;
|
||||
isPendingEatRect = false;
|
||||
}
|
||||
|
||||
// Food stolen
|
||||
@@ -299,30 +366,65 @@ namespace game::state::play
|
||||
{
|
||||
if (!math::is_point_in_rectf(rect, heldItem->position))
|
||||
if (!isAutomation)
|
||||
text.set(dialogue_pool_entry_get(isOverCapacity ? resource::xml::Schema::Element::FOOD_TAKEN_FULL
|
||||
text.set(dialogue_hook_entry_get(isOverCapacity ? resource::xml::Schema::Element::FOOD_TAKEN_FULL
|
||||
: resource::xml::Schema::Element::FOOD_TAKEN),
|
||||
character);
|
||||
}
|
||||
|
||||
if (isEatAreaActive) break;
|
||||
}
|
||||
}
|
||||
else if (category.useMode == "Tool")
|
||||
{
|
||||
auto rect = character.rect();
|
||||
if (item.isToggleSpritesheet && resource::xml::Anm2::is_rect_valid(rect) &&
|
||||
math::is_point_in_rectf(rect, heldItem->position))
|
||||
{
|
||||
character.spritesheet_set(character.spritesheetType == Entity::NORMAL ? Entity::ALTERNATE : Entity::NORMAL);
|
||||
if (auto* alternate = character.data.alternate_spritesheet()) alternate->soundEntry.sound.play();
|
||||
queuedRemoveItemIndex = heldItemIndex;
|
||||
heldItemIndex = -1;
|
||||
pendingEatItemIndex = -1;
|
||||
pendingEatEventID = -1;
|
||||
pendingEatAnimationIndex = -1;
|
||||
isPendingEatRect = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto animation = character.animation_get(); character.time >= animation->frameNum && isQueueFinishFood)
|
||||
{
|
||||
if (!isAutomation)
|
||||
text.set(dialogue_pool_entry_get(isOverCapacity ? resource::xml::Schema::Element::EAT_FULL
|
||||
: resource::xml::Schema::Element::EAT),
|
||||
character);
|
||||
isQueueFinishFood = false;
|
||||
heldItem = vector::find(items, heldItemIndex);
|
||||
if (heldItem && isMouseLeftReleased)
|
||||
{
|
||||
if (fabs(delta.x) >= THROW_THRESHOLD || fabs(delta.y) >= THROW_THRESHOLD)
|
||||
{
|
||||
if (cursorRoot) cursorRoot->soundThrow.play();
|
||||
if (!isAutomation) text.set(dialogue_hook_entry_get(resource::xml::Schema::Element::THROW), character);
|
||||
isJustItemThrown = true;
|
||||
}
|
||||
else if (cursorRoot)
|
||||
cursorRoot->soundRelease.play();
|
||||
|
||||
heldItem->velocity -= delta;
|
||||
heldItemIndex = -1;
|
||||
pendingEatItemIndex = -1;
|
||||
pendingEatEventID = -1;
|
||||
pendingEatAnimationIndex = -1;
|
||||
isPendingEatRect = false;
|
||||
isJustItemHeldStopped = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (queuedRemoveItemIndex > -1)
|
||||
{
|
||||
items.erase(items.begin() + queuedRemoveItemIndex);
|
||||
queuedRemoveItemIndex = -1;
|
||||
pendingEatItemIndex = -1;
|
||||
pendingEatEventID = -1;
|
||||
isPendingEatRect = false;
|
||||
}
|
||||
|
||||
int heldItemMoveIndex = -1;
|
||||
int hoveredTooltipItemID = -1;
|
||||
bool isHoveredTooltipItemHeld = false;
|
||||
for (int i = 0; i < (int)items.size(); i++)
|
||||
{
|
||||
auto& item = items[i];
|
||||
@@ -330,12 +432,16 @@ namespace game::state::play
|
||||
auto& rotationOverride = item.overrides[item.rotationOverrideID];
|
||||
auto& rotation = *rotationOverride.frame.rotation;
|
||||
auto& gravity = schemaItem.isGravity ? schemaItem.gravity : area.gravity;
|
||||
auto isTargetItemLocked = targetItemIndex >= 0 && targetItemIndex < (int)items.size();
|
||||
|
||||
item.update();
|
||||
|
||||
if (math::is_point_in_rectf(item.rect(), cursorPosition) && !isImguiCaptureMouse)
|
||||
if (math::is_point_in_rectf(item.rect(), cursorPosition) && !isImguiCaptureMouse &&
|
||||
(!isTargetItemLocked || i == targetItemIndex))
|
||||
{
|
||||
isItemHovered = true;
|
||||
hoveredTooltipItemID = item.schemaID;
|
||||
isHoveredTooltipItemHeld = heldItemIndex == i;
|
||||
if (cursorRoot)
|
||||
if (auto animation = cursorRoot->animationHover.get())
|
||||
cursor.play({.animation = *animation, .transition = Entity::Transition::IF_IDLE});
|
||||
@@ -360,17 +466,11 @@ namespace game::state::play
|
||||
|
||||
if (isMouseRightClicked)
|
||||
{
|
||||
if (item.durability > 0)
|
||||
{
|
||||
schema.root()->soundDispose.play();
|
||||
particles_queue(schema, resource::xml::Schema::Element::DISPOSE, item.position);
|
||||
}
|
||||
else
|
||||
{
|
||||
schema.root()->soundReturn.play();
|
||||
particles_queue(schema, resource::xml::Schema::Element::RETURN_, item.position);
|
||||
returnItemIDs.emplace_back(item.schemaID);
|
||||
}
|
||||
schema.root()->soundDispose.play();
|
||||
if (cursorRoot)
|
||||
if (auto animation = cursorRoot->animationDispose.get())
|
||||
cursor.play({.animation = *animation, .mode = Entity::PLAY_FORCE, .interrupt = Entity::Interrupt::NEVER});
|
||||
particles_queue(schema, resource::xml::Schema::Element::DISPOSE, item.position);
|
||||
|
||||
if (heldItemIndex == i) heldItemIndex = -1;
|
||||
if (heldItemMoveIndex == i) heldItemMoveIndex = -1;
|
||||
@@ -436,6 +536,13 @@ namespace game::state::play
|
||||
item.position.y = glm::clamp(bounds.y, item.position.y, bounds.w);
|
||||
}
|
||||
|
||||
if (cursor.entityType == CURSOR && hoveredTooltipItemID != -1 && !isHoveredTooltipItemHeld &&
|
||||
ImGui::BeginTooltip())
|
||||
{
|
||||
item_tooltip_draw(character, schema, schema.items[hoveredTooltipItemID]);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
if (heldItemMoveIndex != -1 && heldItemMoveIndex < (int)items.size() - 1)
|
||||
{
|
||||
auto heldItem = std::move(items[heldItemMoveIndex]);
|
||||
|
||||
@@ -19,6 +19,13 @@ namespace game::state::play
|
||||
std::vector<Entity> items{};
|
||||
int heldItemIndex{-1};
|
||||
int queuedRemoveItemIndex{-1};
|
||||
int pendingEatItemIndex{-1};
|
||||
int pendingEatEventID{-1};
|
||||
int pendingEatAnimationIndex{-1};
|
||||
glm::vec4 pendingEatRect{};
|
||||
bool isPendingEatRect{};
|
||||
bool isEatTextPending{};
|
||||
bool isEatTextOverCapacity{};
|
||||
|
||||
bool isItemHovered{};
|
||||
bool isItemHoveredPrevious{};
|
||||
@@ -30,16 +37,14 @@ namespace game::state::play
|
||||
bool isJustItemHeld{};
|
||||
bool isJustItemThrown{};
|
||||
|
||||
bool isQueueFinishFood{};
|
||||
|
||||
bool isItemFinished{};
|
||||
|
||||
glm::vec2 cursorPositionPrevious{};
|
||||
glm::vec2 cursorDeltaPrevious{};
|
||||
|
||||
std::vector<int> queuedItemIDs{};
|
||||
std::vector<int> returnItemIDs{};
|
||||
|
||||
std::vector<bool> discoveredItemIDs{};
|
||||
int spawnDelayTicks{};
|
||||
struct Particle
|
||||
{
|
||||
std::string label{};
|
||||
@@ -53,13 +58,18 @@ namespace game::state::play
|
||||
bool isMouseLeftDown{};
|
||||
bool isMouseLeftReleased{};
|
||||
bool isMouseRightClicked{};
|
||||
bool isMouseRightDown{};
|
||||
bool isAutomation{};
|
||||
int targetItemIndex{-1};
|
||||
};
|
||||
|
||||
std::optional<Input> inputOverride{};
|
||||
std::vector<Particle> particles{};
|
||||
|
||||
int spawn_delay_get(Entity&);
|
||||
bool spawn_possible_get(Entity&);
|
||||
bool spawn_available_get(Entity&);
|
||||
bool spawn_queue(Entity&, int itemID, bool isLimitIgnored = false);
|
||||
bool spawn_queue(Entity&);
|
||||
void update(Entity&, Entity&, AreaManager&, Text&, const glm::vec4& bounds, Canvas&);
|
||||
};
|
||||
}
|
||||
+12
-72
@@ -14,25 +14,19 @@ namespace game::state::play
|
||||
void Menu::update(Resources& resources, ItemManager& itemManager, Entity& character, Text& text, Autofeed& autofeed)
|
||||
{
|
||||
static constexpr auto WIDTH_MULTIPLIER = 0.30f;
|
||||
static constexpr auto HALF_MULTIPLIER = 0.5f;
|
||||
static constexpr auto BAR_DIVIDER_THICKNESS = 1.0f;
|
||||
static constexpr auto PADDING_SIZE_MULTIPLIER = 2.0f;
|
||||
static constexpr auto ZERO_FLOAT = 0.0f;
|
||||
static constexpr auto ONE_FLOAT = 1.0f;
|
||||
static constexpr auto MENU_STYLE_VAR_COUNT = 2;
|
||||
static constexpr auto MENU_BAR_STYLE_VAR_COUNT = 4;
|
||||
static constexpr auto MENU_BAR_FLAGS = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
||||
|
||||
auto& schema = character.data.menuSchema;
|
||||
auto& strings = character.data.strings;
|
||||
|
||||
auto style = ImGui::GetStyle();
|
||||
auto& io = ImGui::GetIO();
|
||||
|
||||
slide.update(isOpen, io.DeltaTime);
|
||||
fullscreenSlide.update(isOpen && isFullscreen, io.DeltaTime);
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding);
|
||||
@@ -40,21 +34,12 @@ namespace game::state::play
|
||||
auto windowSize = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
|
||||
auto barSize =
|
||||
ImVec2(imgui::side_bar_width_get(), windowSize.y - style.WindowPadding.y * PADDING_SIZE_MULTIPLIER);
|
||||
auto fullscreenOffset = barSize.x;
|
||||
|
||||
auto normalSize =
|
||||
ImVec2(windowSize.x * WIDTH_MULTIPLIER, windowSize.y - style.WindowPadding.y * PADDING_SIZE_MULTIPLIER);
|
||||
auto fullscreenSize =
|
||||
ImVec2(windowSize.x - fullscreenOffset, windowSize.y - style.WindowPadding.y * PADDING_SIZE_MULTIPLIER);
|
||||
auto fullscreenT = fullscreenSlide.eased_get();
|
||||
auto size = ImVec2(normalSize.x + (fullscreenSize.x - normalSize.x) * fullscreenT,
|
||||
normalSize.y + (fullscreenSize.y - normalSize.y) * fullscreenT);
|
||||
auto size = normalSize;
|
||||
auto targetX = windowSize.x - size.x;
|
||||
auto t = slide.value_get();
|
||||
auto eased = slide.eased_get();
|
||||
auto isFullscreenVisible = fullscreenSlide.value_get() > ZERO_FLOAT;
|
||||
auto isFullscreenResizeSettled =
|
||||
isFullscreen ? fullscreenSlide.value_get() >= ONE_FLOAT : fullscreenSlide.value_get() <= ZERO_FLOAT;
|
||||
auto posX = windowSize.x + (targetX - windowSize.x) * eased;
|
||||
|
||||
auto pos = ImVec2(posX, style.WindowPadding.y);
|
||||
@@ -74,19 +59,13 @@ namespace game::state::play
|
||||
|
||||
if (WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabInteract).c_str())))
|
||||
{
|
||||
interact.update(resources, text, character, isFullscreenVisible);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabGather).c_str())))
|
||||
{
|
||||
gather.update(isFullscreen, isFullscreenResizeSettled);
|
||||
interact.update(resources, itemManager, text, character, autofeed, false);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
if (WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabInventory).c_str())))
|
||||
{
|
||||
inventory.update(resources, itemManager, character, autofeed);
|
||||
inventory.update(resources, itemManager, character);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
|
||||
@@ -99,7 +78,7 @@ namespace game::state::play
|
||||
|
||||
if (isCheats && WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabCheats).c_str())))
|
||||
{
|
||||
cheats.update(resources, character, inventory);
|
||||
cheats.update(resources, character);
|
||||
ImGui::EndTabItem();
|
||||
}
|
||||
}
|
||||
@@ -119,8 +98,6 @@ namespace game::state::play
|
||||
{
|
||||
auto buttonSize = ImGui::GetContentRegionAvail();
|
||||
auto cursorPos = ImGui::GetCursorScreenPos();
|
||||
auto halfButtonSize = ImVec2(buttonSize.x, buttonSize.y * HALF_MULTIPLIER);
|
||||
auto activeButtonSize = isOpen && !isFullscreenVisible ? halfButtonSize : buttonSize;
|
||||
|
||||
auto tooltip_set = [&](const std::string& tooltip)
|
||||
{
|
||||
@@ -129,65 +106,28 @@ namespace game::state::play
|
||||
ImGui::PopStyleVar();
|
||||
};
|
||||
|
||||
auto isMainResult = WIDGET_FX(ImGui::Button("##MenuToggleNormal", activeButtonSize));
|
||||
auto isMainResult = WIDGET_FX(ImGui::Button("##MenuToggleNormal", buttonSize));
|
||||
if (t <= 0.0f || t >= 1.0f)
|
||||
{
|
||||
tooltip_set(strings.get(isFullscreenVisible ? Strings::MenuRestoreTooltip
|
||||
: isOpen ? Strings::MenuCloseTooltip
|
||||
: Strings::MenuOpenTooltip));
|
||||
tooltip_set(strings.get(isOpen ? Strings::MenuCloseTooltip : Strings::MenuOpenTooltip));
|
||||
if (isMainResult)
|
||||
{
|
||||
if (isFullscreenVisible)
|
||||
{
|
||||
if (isFullscreen) schema.root()->soundClose.play();
|
||||
isFullscreen = false;
|
||||
}
|
||||
isOpen = !isOpen;
|
||||
if (isOpen)
|
||||
character.data.menuSchema.root()->soundOpen.play();
|
||||
else
|
||||
{
|
||||
isOpen = !isOpen;
|
||||
isFullscreen = false;
|
||||
if (isOpen)
|
||||
schema.root()->soundOpen.play();
|
||||
else
|
||||
schema.root()->soundClose.play();
|
||||
}
|
||||
character.data.menuSchema.root()->soundClose.play();
|
||||
}
|
||||
if (!isOpen && !isMainResult && ImGui::IsItemHovered())
|
||||
{
|
||||
isOpen = true;
|
||||
isFullscreen = false;
|
||||
schema.root()->soundOpen.play();
|
||||
}
|
||||
}
|
||||
|
||||
auto fullscreenCursorPos = ImGui::GetCursorScreenPos();
|
||||
auto isFullscreenResult = false;
|
||||
if (isOpen && !isFullscreenVisible)
|
||||
{
|
||||
isFullscreenResult = WIDGET_FX(ImGui::Button("##MenuToggleFullscreen", halfButtonSize));
|
||||
if (t <= 0.0f || t >= 1.0f)
|
||||
{
|
||||
tooltip_set(strings.get(Strings::MenuOpenFullscreenTooltip));
|
||||
if (isFullscreenResult)
|
||||
{
|
||||
isFullscreen = true;
|
||||
schema.root()->soundOpen.play();
|
||||
}
|
||||
character.data.menuSchema.root()->soundOpen.play();
|
||||
}
|
||||
}
|
||||
|
||||
auto direction = !isOpen ? imgui::TriangleDirection::LEFT : imgui::TriangleDirection::RIGHT;
|
||||
imgui::triangle_draw(*ImGui::GetWindowDrawList(), cursorPos, activeButtonSize, direction,
|
||||
imgui::triangle_draw(*ImGui::GetWindowDrawList(), cursorPos, buttonSize, direction,
|
||||
ImGui::GetColorU32(ImGuiCol_Text));
|
||||
if (isOpen && !isFullscreenVisible)
|
||||
{
|
||||
auto separatorY = cursorPos.y + halfButtonSize.y;
|
||||
ImGui::GetWindowDrawList()->AddLine(ImVec2(cursorPos.x, separatorY),
|
||||
ImVec2(cursorPos.x + buttonSize.x, separatorY),
|
||||
ImGui::GetColorU32(ImGuiCol_Separator), BAR_DIVIDER_THICKNESS);
|
||||
imgui::triangle_draw(*ImGui::GetWindowDrawList(), fullscreenCursorPos, halfButtonSize,
|
||||
imgui::TriangleDirection::LEFT, ImGui::GetColorU32(ImGuiCol_Text));
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(MENU_BAR_STYLE_VAR_COUNT);
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "../settings_menu.hpp"
|
||||
|
||||
#include "cheats.hpp"
|
||||
#include "menu/gather.hpp"
|
||||
#include "menu/interact.hpp"
|
||||
#include "menu/inventory.hpp"
|
||||
#include "text.hpp"
|
||||
@@ -18,7 +17,6 @@ namespace game::state::play
|
||||
{
|
||||
public:
|
||||
menu::Interact interact;
|
||||
menu::Gather gather;
|
||||
Cheats cheats;
|
||||
menu::Inventory inventory;
|
||||
state::SettingsMenu settingsMenu;
|
||||
@@ -30,14 +28,9 @@ namespace game::state::play
|
||||
#endif
|
||||
|
||||
bool isOpen{true};
|
||||
bool isFullscreen{};
|
||||
static constexpr auto SLIDE_DURATION = 0.125f;
|
||||
static constexpr auto SLIDE_CLOSED = 0.0f;
|
||||
util::imgui::WindowSlide slide{};
|
||||
util::imgui::WindowSlide fullscreenSlide{SLIDE_DURATION, SLIDE_CLOSED};
|
||||
|
||||
bool is_fullscreen_visible_get() const { return isOpen && (isFullscreen || fullscreenSlide.is_visible()); }
|
||||
float fullscreen_alpha_get() const { return fullscreenSlide.eased_get(); }
|
||||
void update(Resources&, ItemManager&, Entity&, Text&, Autofeed&);
|
||||
};
|
||||
}
|
||||
@@ -1,619 +0,0 @@
|
||||
#include "gather.hpp"
|
||||
|
||||
#include "../../../resource/font.hpp"
|
||||
#include "../../../util/imgui.hpp"
|
||||
#include "../../../util/math.hpp"
|
||||
#include "../../../util/time.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <imgui.h>
|
||||
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
constexpr auto CANVAS_MIN_SIZE = 1.0f;
|
||||
constexpr auto CENTER_RATIO = 0.5f;
|
||||
constexpr auto START_TEXT_Y_RATIO = 0.75f;
|
||||
constexpr auto PLAYER_ACCELERATION = 3.0f;
|
||||
constexpr auto PLAYER_FRICTION_PER_SECOND = 0.72f;
|
||||
constexpr auto PLAYER_SIZE = 32.0f;
|
||||
constexpr auto PLAYER_VELOCITY_MAX = 720.0f;
|
||||
constexpr auto PLAYER_VELOCITY_STOP = 0.0f;
|
||||
constexpr auto PLAYER_BOOST_VELOCITY_GAIN = 320.0f;
|
||||
constexpr auto PLAYER_BOOST_VELOCITY_MIN = 540.0f;
|
||||
constexpr auto PLAYER_BOOST_DURATION = 0.35f;
|
||||
constexpr auto BOOST_DIRECTION_EPSILON = 0.001f;
|
||||
constexpr auto BOOST_DIRECTION_UNIT = 1.0f;
|
||||
constexpr auto GAME_TEXT_FONT_SIZE = resource::Font::HEADER_1;
|
||||
constexpr auto STAMINA_MIN = 0.0f;
|
||||
constexpr auto STAMINA_MAX = 1.0f;
|
||||
constexpr auto STAMINA_REGEN_PER_TICK = 0.002f;
|
||||
constexpr auto STAMINA_BOOST_COST = 0.25f;
|
||||
constexpr auto STAMINA_MAGNET_COST = 0.25f;
|
||||
constexpr auto STAMINA_BAR_WIDTH = 120.0f;
|
||||
constexpr auto STAMINA_BAR_HEIGHT = GAME_TEXT_FONT_SIZE;
|
||||
constexpr auto STAMINA_BAR_PADDING = 8.0f;
|
||||
constexpr auto STAMINA_BAR_PADDING_MULTIPLIER = 2.0f;
|
||||
constexpr auto STAMINA_BAR_ROUNDING = 2.0f;
|
||||
constexpr auto COLLECTIBLE_SIZE = 20.0f;
|
||||
constexpr auto COLLECTIBLE_OSCILLATION_SPEED = 6.0f;
|
||||
constexpr auto COLLECTIBLE_OSCILLATION_AMOUNT = 3.0f;
|
||||
constexpr auto COLLECTIBLE_MAGNET_RADIUS = 72.0f;
|
||||
constexpr auto COLLECTIBLE_MAGNET_RADIUS_EXTENDED = 240.0f;
|
||||
constexpr auto COLLECTIBLE_MAGNET_RADIUS_EXTEND_SECONDS = 1.0f;
|
||||
constexpr auto COLLECTIBLE_MAGNET_RADIUS_WINDOWED_MULTIPLIER = 0.5f;
|
||||
constexpr auto COLLECTIBLE_MAGNET_ACCELERATION = 5200.0f;
|
||||
constexpr auto COLLECTIBLE_MAGNET_ACCELERATION_GAIN = 26000.0f;
|
||||
constexpr auto COLLECTIBLE_MAGNET_ACCELERATION_MAX = 26000.0f;
|
||||
constexpr auto COLLECTIBLE_MAGNET_VELOCITY_MIN = 900.0f;
|
||||
constexpr auto COLLECTIBLE_MAGNET_SPEED_MAX = 1800.0f;
|
||||
constexpr auto COLLECTIBLE_MAGNET_FRICTION_PER_SECOND = 0.98f;
|
||||
constexpr auto COLLECTIBLE_MAGNET_SIDE_VELOCITY_KEEP = 0.35f;
|
||||
constexpr auto COLLECTIBLE_SPAWN_MARGIN_MULTIPLIER = 2.0f;
|
||||
constexpr auto COLLECTIBLE_STRUCTURE_SPACING = 34.0f;
|
||||
constexpr auto COLLECTIBLE_STRUCTURE_COUNT_MIN = 3;
|
||||
constexpr auto COLLECTIBLE_POLYGON_RADIUS_BASE = 28.0f;
|
||||
constexpr auto COLLECTIBLE_POLYGON_RADIUS_PER_COLLECTIBLE = 4.5f;
|
||||
constexpr auto STAMINA_PICKUP_COUNT = 1;
|
||||
constexpr auto COLLECTIBLE_TWO_PI = 6.283185f;
|
||||
constexpr auto COLLECTIBLE_POLYGON_CHANCE = 0.20f;
|
||||
constexpr auto COLLECTIBLE_LINE_CHANCE = 0.55f;
|
||||
constexpr auto SCORE_TEXT_PADDING = 8.0f;
|
||||
constexpr auto SCORE_TEXT_LINE_MULTIPLIER = 1.15f;
|
||||
constexpr auto GAME_TIME_START_SECONDS = 30.0f;
|
||||
constexpr auto GAME_TIME_START_TICKS = GAME_TIME_START_SECONDS * Entity::UPDATE_RATE;
|
||||
constexpr auto GAME_TIME_GAIN_SECONDS = 5.0f;
|
||||
constexpr auto GAME_TIME_GAIN_TICKS = GAME_TIME_GAIN_SECONDS * Entity::UPDATE_RATE;
|
||||
constexpr auto WAVE_RESET_FADE_SECONDS = 0.125f;
|
||||
constexpr auto TIME_GAIN_ALPHA_MAX = 1.0f;
|
||||
constexpr auto TIME_GAIN_FADE_SECONDS = 1.25f;
|
||||
constexpr auto TIME_TEXT_PADDING = 8.0f;
|
||||
constexpr auto COLOR_CHANNEL_MAX = 255;
|
||||
constexpr auto COLOR_GREEN_DIM = 64;
|
||||
constexpr auto COLOR_GREEN = 220;
|
||||
constexpr auto COLOR_COLLECTIBLE_BLUE = 255;
|
||||
constexpr auto COLOR_TIME = 255;
|
||||
constexpr auto COLOR_MAGNET_BLUE = 255;
|
||||
constexpr auto COLOR_MAGNET_ALPHA = 70;
|
||||
constexpr auto MAGNET_CIRCLE_SEGMENTS = 96;
|
||||
constexpr auto MAGNET_CIRCLE_THICKNESS = 2.0f;
|
||||
constexpr auto COLOR_COLLECTIBLE_ALPHA = 255;
|
||||
constexpr auto COLOR_COLLECTIBLE_NEXT_ALPHA = 35;
|
||||
constexpr auto STYLE_VAR_COUNT = 1;
|
||||
constexpr auto CANVAS_FLAGS = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
|
||||
|
||||
float Gather::random_range_get(float min, float max)
|
||||
{
|
||||
if (min >= max) return min;
|
||||
return util::math::random_in_range(min, max);
|
||||
}
|
||||
|
||||
glm::vec2 Gather::random_position_get(float minX, float minY, float maxX, float maxY)
|
||||
{
|
||||
return {random_range_get(minX, maxX), random_range_get(minY, maxY)};
|
||||
}
|
||||
|
||||
Entity Gather::collectible_make(glm::vec2 position)
|
||||
{
|
||||
Entity collectible{};
|
||||
collectible.position = position;
|
||||
collectible.phase = util::math::random_max(COLLECTIBLE_TWO_PI);
|
||||
return collectible;
|
||||
}
|
||||
|
||||
Gather::Structure Gather::structure_get(int remaining)
|
||||
{
|
||||
auto roll = util::math::random();
|
||||
if (remaining < COLLECTIBLE_STRUCTURE_COUNT_MIN) return Structure::SINGLE;
|
||||
if (roll < COLLECTIBLE_POLYGON_CHANCE) return Structure::POLYGON;
|
||||
if (roll < COLLECTIBLE_LINE_CHANCE)
|
||||
return util::math::random_bool() ? Structure::LINE_HORIZONTAL : Structure::LINE_VERTICAL;
|
||||
return Structure::SINGLE;
|
||||
}
|
||||
|
||||
int Gather::structure_count_get(int remaining)
|
||||
{
|
||||
auto countRange = (remaining - COLLECTIBLE_STRUCTURE_COUNT_MIN) + 1;
|
||||
auto countRoll = std::min(countRange - 1, (int)util::math::random_max((float)countRange));
|
||||
return COLLECTIBLE_STRUCTURE_COUNT_MIN + countRoll;
|
||||
}
|
||||
|
||||
int Gather::wave_collectible_count_get(int wave) { return std::max(1, wave); }
|
||||
|
||||
void Gather::collectible_add(std::vector<Entity>& wave, glm::vec2 position, int targetCount)
|
||||
{
|
||||
if ((int)wave.size() >= targetCount) return;
|
||||
wave.emplace_back(collectible_make(position));
|
||||
}
|
||||
|
||||
void Gather::single_spawn(std::vector<Entity>& wave, ImVec2 canvasSize, float margin, int targetCount)
|
||||
{
|
||||
auto position = random_position_get(margin, margin, canvasSize.x - margin, canvasSize.y - margin);
|
||||
collectible_add(wave, position, targetCount);
|
||||
}
|
||||
|
||||
void Gather::line_spawn(std::vector<Entity>& wave, ImVec2 canvasSize, float margin, bool isHorizontal,
|
||||
int structureCount, int targetCount)
|
||||
{
|
||||
auto structureHalf = COLLECTIBLE_STRUCTURE_SPACING * ((float)structureCount - 1) * CENTER_RATIO;
|
||||
auto clearance = structureHalf + (COLLECTIBLE_SIZE * CENTER_RATIO);
|
||||
auto center =
|
||||
isHorizontal
|
||||
? random_position_get(margin + clearance, margin, canvasSize.x - margin - clearance, canvasSize.y - margin)
|
||||
: random_position_get(margin, margin + clearance, canvasSize.x - margin, canvasSize.y - margin - clearance);
|
||||
for (auto i = 0; i < structureCount; ++i)
|
||||
{
|
||||
auto offset = ((float)i - (((float)structureCount - 1) * CENTER_RATIO)) * COLLECTIBLE_STRUCTURE_SPACING;
|
||||
auto position = isHorizontal ? glm::vec2(center.x + offset, center.y) : glm::vec2(center.x, center.y + offset);
|
||||
collectible_add(wave, position, targetCount);
|
||||
}
|
||||
}
|
||||
|
||||
float Gather::polygon_radius_get(int structureCount)
|
||||
{
|
||||
return COLLECTIBLE_POLYGON_RADIUS_BASE + ((float)structureCount * COLLECTIBLE_POLYGON_RADIUS_PER_COLLECTIBLE);
|
||||
}
|
||||
|
||||
void Gather::polygon_spawn(std::vector<Entity>& wave, ImVec2 canvasSize, float margin, int structureCount,
|
||||
int targetCount)
|
||||
{
|
||||
auto radius = polygon_radius_get(structureCount);
|
||||
auto clearance = radius + (COLLECTIBLE_SIZE * CENTER_RATIO);
|
||||
auto center = random_position_get(margin + clearance, margin + clearance, canvasSize.x - margin - clearance,
|
||||
canvasSize.y - margin - clearance);
|
||||
for (auto i = 0; i < structureCount; ++i)
|
||||
{
|
||||
auto angle = COLLECTIBLE_TWO_PI * ((float)i / (float)structureCount);
|
||||
auto position = center + glm::vec2(std::cos(angle), std::sin(angle)) * radius;
|
||||
collectible_add(wave, position, targetCount);
|
||||
}
|
||||
}
|
||||
|
||||
void Gather::wave_spawn(std::vector<Entity>& wave, ImVec2 canvasSize, int waveNumber)
|
||||
{
|
||||
wave.clear();
|
||||
auto targetCount = wave_collectible_count_get(waveNumber);
|
||||
auto margin = PLAYER_SIZE * COLLECTIBLE_SPAWN_MARGIN_MULTIPLIER;
|
||||
while ((int)wave.size() < targetCount)
|
||||
{
|
||||
auto remaining = targetCount - (int)wave.size();
|
||||
switch (structure_get(remaining))
|
||||
{
|
||||
case Structure::LINE_HORIZONTAL:
|
||||
line_spawn(wave, canvasSize, margin, true, structure_count_get(remaining), targetCount);
|
||||
break;
|
||||
case Structure::LINE_VERTICAL:
|
||||
line_spawn(wave, canvasSize, margin, false, structure_count_get(remaining), targetCount);
|
||||
break;
|
||||
case Structure::POLYGON:
|
||||
polygon_spawn(wave, canvasSize, margin, structure_count_get(remaining), targetCount);
|
||||
break;
|
||||
case Structure::SINGLE:
|
||||
default:
|
||||
single_spawn(wave, canvasSize, margin, targetCount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Gather::stamina_pickup_count_get(int) { return STAMINA_PICKUP_COUNT; }
|
||||
|
||||
void Gather::stamina_pickups_spawn(std::vector<Entity>& pickups, ImVec2 canvasSize, int waveNumber)
|
||||
{
|
||||
pickups.clear();
|
||||
auto targetCount = stamina_pickup_count_get(waveNumber);
|
||||
auto margin = PLAYER_SIZE * COLLECTIBLE_SPAWN_MARGIN_MULTIPLIER;
|
||||
while ((int)pickups.size() < targetCount)
|
||||
single_spawn(pickups, canvasSize, margin, targetCount);
|
||||
}
|
||||
|
||||
float Gather::wave_time_gain_ticks_get() { return GAME_TIME_GAIN_TICKS; }
|
||||
|
||||
bool Gather::collectible_hit_get(const Entity& collectible, const Entity& player)
|
||||
{
|
||||
auto playerHalf = PLAYER_SIZE * CENTER_RATIO;
|
||||
auto collectibleHalf = COLLECTIBLE_SIZE * CENTER_RATIO;
|
||||
auto distance = glm::abs(player.position - collectible.position);
|
||||
return distance.x <= playerHalf + collectibleHalf && distance.y <= playerHalf + collectibleHalf;
|
||||
}
|
||||
|
||||
float Gather::magnet_radius_get(float magnetRadiusTime, bool isMenuFullscreen)
|
||||
{
|
||||
auto multiplier = isMenuFullscreen ? STAMINA_MAX : COLLECTIBLE_MAGNET_RADIUS_WINDOWED_MULTIPLIER;
|
||||
auto baseRadius = COLLECTIBLE_MAGNET_RADIUS * multiplier;
|
||||
auto extendedRadius = COLLECTIBLE_MAGNET_RADIUS_EXTENDED * multiplier;
|
||||
auto ratio = std::clamp(magnetRadiusTime / COLLECTIBLE_MAGNET_RADIUS_EXTEND_SECONDS, STAMINA_MIN, STAMINA_MAX);
|
||||
return baseRadius + ((extendedRadius - baseRadius) * ratio);
|
||||
}
|
||||
|
||||
int Gather::collectibles_collect(std::vector<Entity>& wave, const Entity& player)
|
||||
{
|
||||
auto collected = 0;
|
||||
for (auto& collectible : wave)
|
||||
{
|
||||
if (collectible.isCollected || !collectible_hit_get(collectible, player)) continue;
|
||||
collectible.isCollected = true;
|
||||
++collected;
|
||||
}
|
||||
return collected;
|
||||
}
|
||||
|
||||
void Gather::collectibles_magnet_update(std::vector<Entity>& wave, const Entity& player, ImVec2 canvasSize,
|
||||
float magnetRadius, float dt)
|
||||
{
|
||||
auto radiusSquared = magnetRadius * magnetRadius;
|
||||
auto collectibleHalf = COLLECTIBLE_SIZE * CENTER_RATIO;
|
||||
auto minX = collectibleHalf;
|
||||
auto minY = collectibleHalf;
|
||||
auto maxX = std::max(minX, canvasSize.x - collectibleHalf);
|
||||
auto maxY = std::max(minY, canvasSize.y - collectibleHalf);
|
||||
for (auto& collectible : wave)
|
||||
{
|
||||
if (collectible.isCollected) continue;
|
||||
auto direction = player.position - collectible.position;
|
||||
auto distanceSquared = (direction.x * direction.x) + (direction.y * direction.y);
|
||||
if (distanceSquared <= radiusSquared) collectible.isMagnetized = true;
|
||||
if (collectible.isMagnetized && distanceSquared > BOOST_DIRECTION_EPSILON)
|
||||
{
|
||||
collectible.magnetTime += dt;
|
||||
auto magnetAcceleration =
|
||||
std::min(COLLECTIBLE_MAGNET_ACCELERATION_MAX,
|
||||
COLLECTIBLE_MAGNET_ACCELERATION + (COLLECTIBLE_MAGNET_ACCELERATION_GAIN * collectible.magnetTime));
|
||||
auto distance = std::sqrt(distanceSquared);
|
||||
auto directionUnit = direction * (BOOST_DIRECTION_UNIT / distance);
|
||||
auto inwardVelocity = (collectible.velocity.x * directionUnit.x) + (collectible.velocity.y * directionUnit.y);
|
||||
auto inward = directionUnit * inwardVelocity;
|
||||
auto sideways = collectible.velocity - inward;
|
||||
collectible.velocity = inward + (sideways * COLLECTIBLE_MAGNET_SIDE_VELOCITY_KEEP);
|
||||
collectible.velocity += directionUnit * magnetAcceleration * dt;
|
||||
inwardVelocity = (collectible.velocity.x * directionUnit.x) + (collectible.velocity.y * directionUnit.y);
|
||||
if (inwardVelocity < COLLECTIBLE_MAGNET_VELOCITY_MIN)
|
||||
collectible.velocity += directionUnit * (COLLECTIBLE_MAGNET_VELOCITY_MIN - inwardVelocity);
|
||||
}
|
||||
|
||||
auto velocityLength = std::sqrt((collectible.velocity.x * collectible.velocity.x) +
|
||||
(collectible.velocity.y * collectible.velocity.y));
|
||||
if (velocityLength > COLLECTIBLE_MAGNET_SPEED_MAX)
|
||||
{
|
||||
auto velocityScale = COLLECTIBLE_MAGNET_SPEED_MAX / velocityLength;
|
||||
collectible.velocity *= velocityScale;
|
||||
}
|
||||
|
||||
auto velocityRetention = std::pow(COLLECTIBLE_MAGNET_FRICTION_PER_SECOND, dt);
|
||||
collectible.velocity *= velocityRetention;
|
||||
collectible.position += collectible.velocity * dt;
|
||||
if (collectible.position.x < minX || collectible.position.x > maxX) collectible.velocity.x = PLAYER_VELOCITY_STOP;
|
||||
if (collectible.position.y < minY || collectible.position.y > maxY) collectible.velocity.y = PLAYER_VELOCITY_STOP;
|
||||
collectible.position.x = std::clamp(collectible.position.x, minX, maxX);
|
||||
collectible.position.y = std::clamp(collectible.position.y, minY, maxY);
|
||||
}
|
||||
}
|
||||
|
||||
bool Gather::wave_complete_get(const std::vector<Entity>& wave)
|
||||
{
|
||||
if (wave.empty()) return false;
|
||||
for (auto& collectible : wave)
|
||||
if (!collectible.isCollected) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Gather::collectibles_draw(const std::vector<Entity>& wave, ImDrawList* drawList, ImVec2 canvasMin, float time,
|
||||
ImU32 color)
|
||||
{
|
||||
for (auto& collectible : wave)
|
||||
{
|
||||
if (collectible.isCollected) continue;
|
||||
auto size = COLLECTIBLE_SIZE + (std::sin((time * COLLECTIBLE_OSCILLATION_SPEED) + collectible.phase) *
|
||||
COLLECTIBLE_OSCILLATION_AMOUNT);
|
||||
auto half = size * CENTER_RATIO;
|
||||
auto min = ImVec2(canvasMin.x + collectible.position.x - half, canvasMin.y + collectible.position.y - half);
|
||||
auto max = ImVec2(min.x + size, min.y + size);
|
||||
drawList->AddRectFilled(min, max, color);
|
||||
}
|
||||
}
|
||||
|
||||
void Gather::magnet_radius_draw(ImDrawList* drawList, ImVec2 canvasMin, const Entity& player, float radius)
|
||||
{
|
||||
auto center = ImVec2(canvasMin.x + player.position.x, canvasMin.y + player.position.y);
|
||||
drawList->AddCircle(center, radius, IM_COL32(0, 0, COLOR_MAGNET_BLUE, COLOR_MAGNET_ALPHA), MAGNET_CIRCLE_SEGMENTS,
|
||||
MAGNET_CIRCLE_THICKNESS);
|
||||
}
|
||||
|
||||
int Gather::alpha_get(int alpha, float multiplier)
|
||||
{
|
||||
return (int)((float)alpha * std::clamp(multiplier, STAMINA_MIN, STAMINA_MAX));
|
||||
}
|
||||
|
||||
void Gather::progress_bar_draw(ImDrawList* drawList, ImVec2 min, ImVec2 max, float progress, ImU32 backgroundColor,
|
||||
ImU32 fillColor, const std::string& text)
|
||||
{
|
||||
auto fillMax = ImVec2(min.x + ((max.x - min.x) * std::clamp(progress, STAMINA_MIN, STAMINA_MAX)), max.y);
|
||||
drawList->AddRectFilled(min, max, backgroundColor, STAMINA_BAR_ROUNDING);
|
||||
drawList->AddRectFilled(min, fillMax, fillColor, STAMINA_BAR_ROUNDING);
|
||||
if (text.empty()) return;
|
||||
|
||||
auto textSize = util::imgui::draw_list_text_size_get(text, GAME_TEXT_FONT_SIZE);
|
||||
auto textPos = ImVec2(min.x + ((max.x - min.x - textSize.x) * CENTER_RATIO),
|
||||
min.y + ((max.y - min.y - textSize.y) * CENTER_RATIO));
|
||||
util::imgui::draw_list_text_draw(
|
||||
*drawList, textPos, text, GAME_TEXT_FONT_SIZE,
|
||||
IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));
|
||||
}
|
||||
|
||||
void Gather::reset()
|
||||
{
|
||||
player = {};
|
||||
collectibles.clear();
|
||||
nextCollectibles.clear();
|
||||
staminaPickups.clear();
|
||||
nextStaminaPickups.clear();
|
||||
targetPosition = {};
|
||||
stamina = STAMINA_MAX;
|
||||
boostTime = STAMINA_MIN;
|
||||
magnetRadiusTime = STAMINA_MIN;
|
||||
timeTicks = GAME_TIME_START_TICKS;
|
||||
timeGainTicks = STAMINA_MIN;
|
||||
timeGainAlpha = STAMINA_MIN;
|
||||
score = 0;
|
||||
wave = 1;
|
||||
isStarted = false;
|
||||
isPaused = false;
|
||||
isMenuFullscreenKnown = false;
|
||||
isMenuFullscreenLast = false;
|
||||
isWaveResetPending = false;
|
||||
waveResetAlpha = STAMINA_MAX;
|
||||
}
|
||||
|
||||
void Gather::start(ImVec2 canvasSize)
|
||||
{
|
||||
reset();
|
||||
player.position = glm::vec2(canvasSize.x * CENTER_RATIO, canvasSize.y * CENTER_RATIO);
|
||||
targetPosition = player.position;
|
||||
isStarted = true;
|
||||
wave_reset(canvasSize);
|
||||
}
|
||||
|
||||
void Gather::wave_reset(ImVec2 canvasSize)
|
||||
{
|
||||
wave_spawn(collectibles, canvasSize, wave);
|
||||
wave_spawn(nextCollectibles, canvasSize, wave + 1);
|
||||
stamina_pickups_spawn(staminaPickups, canvasSize, wave);
|
||||
stamina_pickups_spawn(nextStaminaPickups, canvasSize, wave + 1);
|
||||
magnetRadiusTime = STAMINA_MIN;
|
||||
isWaveResetPending = false;
|
||||
waveResetAlpha = STAMINA_MAX;
|
||||
}
|
||||
|
||||
void Gather::wave_reset_request()
|
||||
{
|
||||
isWaveResetPending = true;
|
||||
waveResetAlpha = STAMINA_MAX;
|
||||
magnetRadiusTime = STAMINA_MIN;
|
||||
}
|
||||
|
||||
void Gather::score_draw(ImDrawList* drawList, ImVec2 canvasMin, int score, int wave)
|
||||
{
|
||||
auto scoreText = std::format("{}", score);
|
||||
auto position = ImVec2(canvasMin.x + SCORE_TEXT_PADDING, canvasMin.y + SCORE_TEXT_PADDING);
|
||||
util::imgui::draw_list_text_draw(
|
||||
*drawList, position, scoreText, GAME_TEXT_FONT_SIZE,
|
||||
IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));
|
||||
auto waveText = std::format("Wave #{}", wave);
|
||||
auto wavePosition = ImVec2(position.x, position.y + (GAME_TEXT_FONT_SIZE * SCORE_TEXT_LINE_MULTIPLIER));
|
||||
util::imgui::draw_list_text_draw(
|
||||
*drawList, wavePosition, waveText, GAME_TEXT_FONT_SIZE,
|
||||
IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));
|
||||
}
|
||||
|
||||
void Gather::time_draw(ImDrawList* drawList, ImVec2 canvasMin, ImVec2 canvasMax, float timeTicks, float timeGainTicks,
|
||||
float timeGainAlpha)
|
||||
{
|
||||
auto timeText = util::time::ticks_to_minutes_seconds_centiseconds((int)std::ceil(timeTicks));
|
||||
auto textSize = util::imgui::draw_list_text_size_get(timeText, GAME_TEXT_FONT_SIZE);
|
||||
auto textWidth = util::imgui::draw_list_digit_fixed_width_get(timeText, GAME_TEXT_FONT_SIZE);
|
||||
auto textPos = ImVec2(canvasMax.x - textWidth - TIME_TEXT_PADDING, canvasMin.y + TIME_TEXT_PADDING);
|
||||
util::imgui::draw_list_digit_fixed_width_draw(*drawList, textPos, timeText, GAME_TEXT_FONT_SIZE,
|
||||
IM_COL32(COLOR_TIME, COLOR_TIME, COLOR_TIME, COLOR_CHANNEL_MAX));
|
||||
|
||||
if (timeGainAlpha <= STAMINA_MIN) return;
|
||||
auto gainText =
|
||||
std::format("+{}", util::time::ticks_to_minutes_seconds_centiseconds((int)std::ceil(timeGainTicks)));
|
||||
auto alpha = (int)(COLOR_CHANNEL_MAX * std::clamp(timeGainAlpha, STAMINA_MIN, STAMINA_MAX));
|
||||
auto gainTextWidth = util::imgui::draw_list_digit_fixed_width_get(gainText, GAME_TEXT_FONT_SIZE);
|
||||
auto gainPos = ImVec2(canvasMax.x - gainTextWidth - TIME_TEXT_PADDING, textPos.y + textSize.y + TIME_TEXT_PADDING);
|
||||
util::imgui::draw_list_digit_fixed_width_draw(*drawList, gainPos, gainText, GAME_TEXT_FONT_SIZE,
|
||||
IM_COL32(COLOR_TIME, COLOR_TIME, COLOR_TIME, alpha));
|
||||
}
|
||||
|
||||
void Gather::update(bool isMenuFullscreen, bool isMenuResizeSettled)
|
||||
{
|
||||
auto canvasSize = ImGui::GetContentRegionAvail();
|
||||
if (canvasSize.x <= CANVAS_MIN_SIZE || canvasSize.y <= CANVAS_MIN_SIZE) return;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2());
|
||||
if (ImGui::BeginChild("##Gather Canvas Child", canvasSize, ImGuiChildFlags_None, CANVAS_FLAGS))
|
||||
{
|
||||
canvasSize = ImGui::GetContentRegionAvail();
|
||||
auto canvasMin = ImGui::GetCursorScreenPos();
|
||||
auto canvasMax = ImVec2(canvasMin.x + canvasSize.x, canvasMin.y + canvasSize.y);
|
||||
auto drawList = ImGui::GetWindowDrawList();
|
||||
auto dt = ImGui::GetIO().DeltaTime;
|
||||
auto isCanvasFocused = ImGui::IsWindowFocused();
|
||||
|
||||
ImGui::InvisibleButton("##Gather Canvas", canvasSize);
|
||||
auto isCanvasHovered = ImGui::IsItemHovered();
|
||||
|
||||
drawList->AddRectFilled(canvasMin, canvasMax, IM_COL32(0, 0, 0, COLOR_CHANNEL_MAX / 2));
|
||||
|
||||
if (!isMenuFullscreenKnown)
|
||||
{
|
||||
isMenuFullscreenKnown = true;
|
||||
isMenuFullscreenLast = isMenuFullscreen;
|
||||
}
|
||||
auto isMenuFullscreenChanged = isMenuFullscreenLast != isMenuFullscreen;
|
||||
isMenuFullscreenLast = isMenuFullscreen;
|
||||
|
||||
if (isStarted && isMenuFullscreenChanged) wave_reset_request();
|
||||
|
||||
if (isStarted && isWaveResetPending)
|
||||
{
|
||||
waveResetAlpha = std::max(STAMINA_MIN, waveResetAlpha - (dt / WAVE_RESET_FADE_SECONDS));
|
||||
if (isMenuResizeSettled) wave_reset(canvasSize);
|
||||
}
|
||||
|
||||
if (!isStarted && isCanvasHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) start(canvasSize);
|
||||
|
||||
if (isStarted)
|
||||
{
|
||||
if (isCanvasFocused && ImGui::IsKeyPressed(ImGuiKey_P, false)) isPaused = !isPaused;
|
||||
|
||||
auto playerHalf = PLAYER_SIZE * CENTER_RATIO;
|
||||
auto minX = std::min(playerHalf, canvasSize.x * CENTER_RATIO);
|
||||
auto minY = std::min(playerHalf, canvasSize.y * CENTER_RATIO);
|
||||
auto maxX = std::max(minX, canvasSize.x - playerHalf);
|
||||
auto maxY = std::max(minY, canvasSize.y - playerHalf);
|
||||
|
||||
player.position.x = std::clamp(player.position.x, minX, maxX);
|
||||
player.position.y = std::clamp(player.position.y, minY, maxY);
|
||||
targetPosition.x = std::clamp(targetPosition.x, minX, maxX);
|
||||
targetPosition.y = std::clamp(targetPosition.y, minY, maxY);
|
||||
|
||||
if (!isPaused)
|
||||
{
|
||||
if (isCanvasHovered)
|
||||
{
|
||||
auto mouse = ImGui::GetIO().MousePos;
|
||||
targetPosition =
|
||||
glm::vec2(std::clamp(mouse.x - canvasMin.x, minX, maxX), std::clamp(mouse.y - canvasMin.y, minY, maxY));
|
||||
}
|
||||
|
||||
timeTicks = std::max(STAMINA_MIN, timeTicks - (Entity::UPDATE_RATE * dt));
|
||||
if (timeTicks <= STAMINA_MIN) reset();
|
||||
|
||||
if (isStarted)
|
||||
{
|
||||
timeGainAlpha = std::max(STAMINA_MIN, timeGainAlpha - (dt / TIME_GAIN_FADE_SECONDS));
|
||||
stamina =
|
||||
std::clamp(stamina + (STAMINA_REGEN_PER_TICK * Entity::UPDATE_RATE * dt), STAMINA_MIN, STAMINA_MAX);
|
||||
if (isCanvasHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Middle) && stamina >= STAMINA_MAGNET_COST)
|
||||
{
|
||||
magnetRadiusTime = COLLECTIBLE_MAGNET_RADIUS_EXTEND_SECONDS;
|
||||
stamina = std::max(STAMINA_MIN, stamina - STAMINA_MAGNET_COST);
|
||||
}
|
||||
if (isCanvasHovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right) && stamina >= STAMINA_BOOST_COST)
|
||||
{
|
||||
auto boostDirection = targetPosition - player.position;
|
||||
auto boostDirectionLength =
|
||||
std::sqrt((boostDirection.x * boostDirection.x) + (boostDirection.y * boostDirection.y));
|
||||
if (boostDirectionLength > BOOST_DIRECTION_EPSILON)
|
||||
{
|
||||
auto boostDirectionScale = BOOST_DIRECTION_UNIT / boostDirectionLength;
|
||||
auto boostDirectionUnit = boostDirection * boostDirectionScale;
|
||||
player.velocity.x += boostDirectionUnit.x * PLAYER_BOOST_VELOCITY_GAIN;
|
||||
player.velocity.y += boostDirectionUnit.y * PLAYER_BOOST_VELOCITY_GAIN;
|
||||
auto boostVelocity =
|
||||
(player.velocity.x * boostDirectionUnit.x) + (player.velocity.y * boostDirectionUnit.y);
|
||||
if (boostVelocity < PLAYER_BOOST_VELOCITY_MIN)
|
||||
{
|
||||
auto boostVelocityAdd = PLAYER_BOOST_VELOCITY_MIN - boostVelocity;
|
||||
player.velocity.x += boostDirectionUnit.x * boostVelocityAdd;
|
||||
player.velocity.y += boostDirectionUnit.y * boostVelocityAdd;
|
||||
}
|
||||
boostTime = PLAYER_BOOST_DURATION;
|
||||
stamina = std::max(STAMINA_MIN, stamina - STAMINA_BOOST_COST);
|
||||
}
|
||||
}
|
||||
|
||||
player.velocity.x += (targetPosition.x - player.position.x) * PLAYER_ACCELERATION * dt;
|
||||
player.velocity.y += (targetPosition.y - player.position.y) * PLAYER_ACCELERATION * dt;
|
||||
auto velocityRetention = std::pow(PLAYER_FRICTION_PER_SECOND, dt);
|
||||
player.velocity.x *= velocityRetention;
|
||||
player.velocity.y *= velocityRetention;
|
||||
auto velocityLength =
|
||||
std::sqrt((player.velocity.x * player.velocity.x) + (player.velocity.y * player.velocity.y));
|
||||
auto isBoostActive = boostTime > STAMINA_MIN;
|
||||
if (!isBoostActive && velocityLength > PLAYER_VELOCITY_MAX)
|
||||
{
|
||||
auto velocityScale = PLAYER_VELOCITY_MAX / velocityLength;
|
||||
player.velocity.x *= velocityScale;
|
||||
player.velocity.y *= velocityScale;
|
||||
}
|
||||
boostTime = std::max(STAMINA_MIN, boostTime - dt);
|
||||
player.position.x += player.velocity.x * dt;
|
||||
player.position.y += player.velocity.y * dt;
|
||||
|
||||
if (player.position.x < minX || player.position.x > maxX) player.velocity.x = PLAYER_VELOCITY_STOP;
|
||||
if (player.position.y < minY || player.position.y > maxY) player.velocity.y = PLAYER_VELOCITY_STOP;
|
||||
player.position.x = std::clamp(player.position.x, minX, maxX);
|
||||
player.position.y = std::clamp(player.position.y, minY, maxY);
|
||||
|
||||
if (!isWaveResetPending)
|
||||
{
|
||||
auto magnetRadius = magnet_radius_get(magnetRadiusTime, isMenuFullscreen);
|
||||
collectibles_magnet_update(collectibles, player, canvasSize, magnetRadius, dt);
|
||||
magnetRadiusTime = std::max(STAMINA_MIN, magnetRadiusTime - dt);
|
||||
score += collectibles_collect(collectibles, player);
|
||||
if (collectibles_collect(staminaPickups, player) > 0) stamina = STAMINA_MAX;
|
||||
if (wave_complete_get(collectibles))
|
||||
{
|
||||
timeGainTicks = wave_time_gain_ticks_get();
|
||||
timeTicks += timeGainTicks;
|
||||
timeGainAlpha = TIME_GAIN_ALPHA_MAX;
|
||||
++wave;
|
||||
collectibles = nextCollectibles;
|
||||
staminaPickups = nextStaminaPickups;
|
||||
wave_spawn(nextCollectibles, canvasSize, wave + 1);
|
||||
stamina_pickups_spawn(nextStaminaPickups, canvasSize, wave + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isStarted)
|
||||
{
|
||||
auto playerHalf = PLAYER_SIZE * CENTER_RATIO;
|
||||
auto collectibleAlpha = alpha_get(COLOR_COLLECTIBLE_ALPHA, waveResetAlpha);
|
||||
auto nextCollectibleAlpha = alpha_get(COLOR_COLLECTIBLE_NEXT_ALPHA, waveResetAlpha);
|
||||
collectibles_draw(nextCollectibles, drawList, canvasMin, (float)ImGui::GetTime(),
|
||||
IM_COL32(0, 0, COLOR_COLLECTIBLE_BLUE, nextCollectibleAlpha));
|
||||
collectibles_draw(nextStaminaPickups, drawList, canvasMin, (float)ImGui::GetTime(),
|
||||
IM_COL32(0, COLOR_GREEN, 0, nextCollectibleAlpha));
|
||||
collectibles_draw(collectibles, drawList, canvasMin, (float)ImGui::GetTime(),
|
||||
IM_COL32(0, 0, COLOR_COLLECTIBLE_BLUE, collectibleAlpha));
|
||||
collectibles_draw(staminaPickups, drawList, canvasMin, (float)ImGui::GetTime(),
|
||||
IM_COL32(0, COLOR_GREEN, 0, collectibleAlpha));
|
||||
magnet_radius_draw(drawList, canvasMin, player, magnet_radius_get(magnetRadiusTime, isMenuFullscreen));
|
||||
|
||||
auto playerMin =
|
||||
ImVec2(canvasMin.x + player.position.x - playerHalf, canvasMin.y + player.position.y - playerHalf);
|
||||
auto playerMax = ImVec2(playerMin.x + PLAYER_SIZE, playerMin.y + PLAYER_SIZE);
|
||||
drawList->AddRectFilled(playerMin, playerMax,
|
||||
IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));
|
||||
|
||||
auto staminaBarWidth =
|
||||
std::min(STAMINA_BAR_WIDTH,
|
||||
std::max(CANVAS_MIN_SIZE, canvasSize.x - (STAMINA_BAR_PADDING * STAMINA_BAR_PADDING_MULTIPLIER)));
|
||||
auto staminaBarMin = ImVec2(canvasMax.x - staminaBarWidth - STAMINA_BAR_PADDING,
|
||||
canvasMax.y - STAMINA_BAR_HEIGHT - STAMINA_BAR_PADDING);
|
||||
auto staminaBarMax = ImVec2(staminaBarMin.x + staminaBarWidth, staminaBarMin.y + STAMINA_BAR_HEIGHT);
|
||||
progress_bar_draw(drawList, staminaBarMin, staminaBarMax, stamina,
|
||||
IM_COL32(0, COLOR_GREEN_DIM, 0, COLOR_CHANNEL_MAX),
|
||||
IM_COL32(0, COLOR_GREEN, 0, COLOR_CHANNEL_MAX));
|
||||
score_draw(drawList, canvasMin, score, wave);
|
||||
time_draw(drawList, canvasMin, canvasMax, timeTicks, timeGainTicks, timeGainAlpha);
|
||||
if (isPaused)
|
||||
util::imgui::draw_list_text_center_draw(
|
||||
*drawList, canvasMin, canvasSize, "PAUSED", GAME_TEXT_FONT_SIZE, CENTER_RATIO, CENTER_RATIO,
|
||||
IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));
|
||||
}
|
||||
else
|
||||
util::imgui::draw_list_text_center_draw(
|
||||
*drawList, canvasMin, canvasSize, "Click to start!", GAME_TEXT_FONT_SIZE, CENTER_RATIO, START_TEXT_Y_RATIO,
|
||||
IM_COL32(COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX, COLOR_CHANNEL_MAX));
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(STYLE_VAR_COUNT);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../../entity.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
class Gather
|
||||
{
|
||||
public:
|
||||
Entity player{};
|
||||
std::vector<Entity> collectibles{};
|
||||
std::vector<Entity> nextCollectibles{};
|
||||
std::vector<Entity> staminaPickups{};
|
||||
std::vector<Entity> nextStaminaPickups{};
|
||||
glm::vec2 targetPosition{};
|
||||
float stamina{};
|
||||
float boostTime{};
|
||||
float magnetRadiusTime{};
|
||||
float timeTicks{};
|
||||
float timeGainTicks{};
|
||||
float timeGainAlpha{};
|
||||
int score{};
|
||||
int wave{};
|
||||
bool isStarted{};
|
||||
bool isPaused{};
|
||||
bool isMenuFullscreenKnown{};
|
||||
bool isMenuFullscreenLast{};
|
||||
bool isWaveResetPending{};
|
||||
float waveResetAlpha{};
|
||||
|
||||
void update(bool isMenuFullscreen, bool isMenuResizeSettled);
|
||||
|
||||
private:
|
||||
enum class Structure
|
||||
{
|
||||
SINGLE,
|
||||
LINE_HORIZONTAL,
|
||||
LINE_VERTICAL,
|
||||
POLYGON
|
||||
};
|
||||
|
||||
static float random_range_get(float, float);
|
||||
static glm::vec2 random_position_get(float, float, float, float);
|
||||
static Entity collectible_make(glm::vec2);
|
||||
static Structure structure_get(int);
|
||||
static int structure_count_get(int);
|
||||
static int wave_collectible_count_get(int);
|
||||
static void collectible_add(std::vector<Entity>&, glm::vec2, int);
|
||||
static void single_spawn(std::vector<Entity>&, ImVec2, float, int);
|
||||
static void line_spawn(std::vector<Entity>&, ImVec2, float, bool, int, int);
|
||||
static float polygon_radius_get(int);
|
||||
static void polygon_spawn(std::vector<Entity>&, ImVec2, float, int, int);
|
||||
static void wave_spawn(std::vector<Entity>&, ImVec2, int);
|
||||
static int stamina_pickup_count_get(int);
|
||||
static void stamina_pickups_spawn(std::vector<Entity>&, ImVec2, int);
|
||||
static float wave_time_gain_ticks_get();
|
||||
static bool collectible_hit_get(const Entity&, const Entity&);
|
||||
static float magnet_radius_get(float, bool isMenuFullscreen);
|
||||
static int collectibles_collect(std::vector<Entity>&, const Entity&);
|
||||
static void collectibles_magnet_update(std::vector<Entity>&, const Entity&, ImVec2, float, float);
|
||||
static bool wave_complete_get(const std::vector<Entity>&);
|
||||
static void collectibles_draw(const std::vector<Entity>&, ImDrawList*, ImVec2, float, ImU32);
|
||||
static void magnet_radius_draw(ImDrawList*, ImVec2, const Entity&, float);
|
||||
static int alpha_get(int, float);
|
||||
static void progress_bar_draw(ImDrawList*, ImVec2, ImVec2, float, ImU32, ImU32, const std::string& = {});
|
||||
static void score_draw(ImDrawList*, ImVec2, int, int);
|
||||
static void time_draw(ImDrawList*, ImVec2, ImVec2, float, float, float);
|
||||
void reset();
|
||||
void start(ImVec2);
|
||||
void wave_reset(ImVec2);
|
||||
void wave_reset_request();
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
#include "interact.hpp"
|
||||
|
||||
#include "../info.hpp"
|
||||
|
||||
#include "../../../util/color.hpp"
|
||||
#include "../../../util/imgui.hpp"
|
||||
#include "../../../util/imgui/widget.hpp"
|
||||
#include "../../../util/math.hpp"
|
||||
#include "../../../util/string.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <format>
|
||||
|
||||
using namespace game::resource;
|
||||
using namespace game::resource::xml;
|
||||
@@ -11,25 +19,172 @@ using namespace game::util::imgui;
|
||||
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
void Interact::update(Resources& resources, Text& text, Entity& character, bool)
|
||||
std::string playtime_format(float totalPlaytimeSeconds)
|
||||
{
|
||||
static constexpr auto PLAYTIME_SECONDS_PER_MINUTE = 60;
|
||||
static constexpr auto PLAYTIME_MINUTES_PER_HOUR = 60;
|
||||
static constexpr auto PLAYTIME_SECONDS_PER_HOUR = PLAYTIME_SECONDS_PER_MINUTE * PLAYTIME_MINUTES_PER_HOUR;
|
||||
|
||||
auto totalSeconds = std::max(0, (int)std::floor(totalPlaytimeSeconds));
|
||||
auto seconds = totalSeconds % PLAYTIME_SECONDS_PER_MINUTE;
|
||||
if (totalSeconds < PLAYTIME_SECONDS_PER_MINUTE) return std::format("{:02}", seconds);
|
||||
|
||||
auto minutes = totalSeconds / PLAYTIME_SECONDS_PER_MINUTE % PLAYTIME_MINUTES_PER_HOUR;
|
||||
if (totalSeconds < PLAYTIME_SECONDS_PER_HOUR) return std::format("{:02}:{:02}", minutes, seconds);
|
||||
|
||||
auto hours = totalSeconds / PLAYTIME_SECONDS_PER_HOUR;
|
||||
return std::format("{:02}:{:02}:{:02}", hours, minutes, seconds);
|
||||
}
|
||||
|
||||
float interact_info_height_get()
|
||||
{
|
||||
return Font::HEADER_2 + ImGui::GetFrameHeightWithSpacing();
|
||||
}
|
||||
|
||||
void interact_info_content_draw(Resources& resources, Entity& character, ImVec2 size)
|
||||
{
|
||||
static constexpr auto HALF_MULTIPLIER = 0.5f;
|
||||
static constexpr auto ZERO_FLOAT = 0.0f;
|
||||
|
||||
auto& strings = character.data.strings;
|
||||
auto& style = ImGui::GetStyle();
|
||||
auto childSize = ImVec2((size.x - style.ItemSpacing.x) * HALF_MULTIPLIER, size.y);
|
||||
|
||||
if (ImGui::BeginChild("##Weight", childSize))
|
||||
{
|
||||
auto* settings = resources.settings.root();
|
||||
auto system = settings->measurementSystem == "Imperial" ? measurement::IMPERIAL : measurement::METRIC;
|
||||
auto weight = character.weight_get(system);
|
||||
auto stage = character.stage_get();
|
||||
auto stageMax = character.stage_max_get();
|
||||
auto stageCount = character.stage_count_get();
|
||||
auto stageWeight = character.stage_threshold_get(stage, system);
|
||||
auto stageNextWeight = character.stage_threshold_next_get(system);
|
||||
auto unitString = (system == measurement::IMPERIAL ? "lbs" : "kg");
|
||||
|
||||
auto weightString = util::string::format_commas(weight, 2) + " " + unitString;
|
||||
imgui::text_unformatted_fit_draw(weightString.c_str(), ImVec2(ImGui::GetContentRegionAvail().x, Font::HEADER_2),
|
||||
Font::HEADER_2);
|
||||
ImGui::SetItemTooltip("%s", weightString.c_str());
|
||||
|
||||
auto stageProgress = character.stage_progress_get();
|
||||
ImGui::ProgressBar(stageProgress, ImVec2(ImGui::GetContentRegionAvail().x, ZERO_FLOAT),
|
||||
strings.get(stage >= stageMax ? Strings::InfoProgressMax : Strings::InfoProgressToNextStage)
|
||||
.c_str());
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
ImGui::Text(strings.get(Strings::InfoStageProgressFormat).c_str(), stage + 1, stageCount,
|
||||
math::to_percent(stageProgress));
|
||||
ImGui::Separator();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, imgui::to_imvec4(color::GRAY));
|
||||
if (stage >= stageMax)
|
||||
ImGui::TextUnformatted(strings.get(Strings::InfoMaxedOut).c_str());
|
||||
else
|
||||
{
|
||||
ImGui::Text(strings.get(Strings::InfoStageStartFormat).c_str(), stageWeight, unitString);
|
||||
ImGui::Text(strings.get(Strings::InfoStageCurrentFormat).c_str(), weight, unitString);
|
||||
ImGui::Text(strings.get(Strings::InfoStageNextFormat).c_str(), stageNextWeight, unitString);
|
||||
}
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (ImGui::BeginChild("##Calories and Capacity", childSize))
|
||||
{
|
||||
auto& calories = character.calories;
|
||||
auto& capacity = character.capacity;
|
||||
|
||||
auto overstuffedPercent = std::max(ZERO_FLOAT, (calories - capacity) / (character.max_capacity() - capacity));
|
||||
auto caloriesColor = ImVec4(1.0f, 1.0f - overstuffedPercent, 1.0f - overstuffedPercent, 1.0f);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, caloriesColor);
|
||||
auto caloriesString = std::format("{:.0f} kcal / {:.0f} kcal", calories,
|
||||
character.is_over_capacity() ? character.max_capacity() : character.capacity);
|
||||
imgui::text_unformatted_fit_draw(caloriesString.c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x, Font::HEADER_2), Font::HEADER_2);
|
||||
ImGui::SetItemTooltip("%s", caloriesString.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
auto digestionProgress = character.isDigesting
|
||||
? (float)character.digestionTimer / character.data.root()->digestionTimerMax
|
||||
: character.digestionProgress / Entity::DIGESTION_MAX;
|
||||
ImGui::ProgressBar(digestionProgress, ImVec2(ImGui::GetContentRegionAvail().x, ZERO_FLOAT),
|
||||
strings.get(character.isDigesting ? Strings::InfoDigesting : Strings::InfoDigestion).c_str());
|
||||
|
||||
if (ImGui::BeginItemTooltip())
|
||||
{
|
||||
if (character.isDigesting)
|
||||
ImGui::TextUnformatted(strings.get(Strings::InfoDigestionInProgress).c_str());
|
||||
else if (digestionProgress <= ZERO_FLOAT)
|
||||
ImGui::TextUnformatted(strings.get(Strings::InfoGiveFoodToStartDigesting).c_str());
|
||||
else
|
||||
ImGui::Text("%0.2f%%", math::to_percent(digestionProgress));
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY)));
|
||||
ImGui::Text(strings.get(Strings::InfoDigestionRateFormat).c_str(), character.digestion_rate_get());
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
ImGui::SeparatorText(strings.get(Strings::InteractStats).c_str());
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::Text(strings.get(Strings::InteractTotalCaloriesFormat).c_str(), character.totalCaloriesConsumed);
|
||||
ImGui::Text(strings.get(Strings::InteractTotalItemsSpawnedFormat).c_str(), character.totalItemsSpawned);
|
||||
ImGui::Text(strings.get(Strings::InteractTotalItemsConsumedFormat).c_str(), character.totalItemsConsumed);
|
||||
auto totalPlaytime = playtime_format(character.totalPlaytimeSeconds);
|
||||
ImGui::Text(strings.get(Strings::InteractTotalPlaytimeFormat).c_str(), totalPlaytime.c_str());
|
||||
}
|
||||
|
||||
void Interact::update(Resources& resources, ItemManager& itemManager, Text& text, Entity& character, Autofeed& autofeed,
|
||||
bool)
|
||||
{
|
||||
static constexpr auto ZERO_FLOAT = 0.0f;
|
||||
static constexpr auto ONE_FLOAT = 1.0f;
|
||||
static constexpr auto TOOLTIP_BUFFER_SIZE = 128;
|
||||
|
||||
auto& dialogue = character.data.dialogue;
|
||||
auto& strings = character.data.strings;
|
||||
auto* random = dialogue.get(Schema::Element::RANDOM);
|
||||
auto* help = dialogue.get(Schema::Element::HELP);
|
||||
auto* random = dialogue.dialogue_hook_get(Schema::Element::RANDOM);
|
||||
auto* help = dialogue.dialogue_hook_get(Schema::Element::HELP);
|
||||
auto stage = glm::clamp(0, character.stage_get(), character.stage_max_get());
|
||||
auto poolID = character.data.stage_pool_id_get(stage);
|
||||
auto isFeelingAvailable = poolID != -1;
|
||||
auto isDialogueAvailable = random || help || isFeelingAvailable;
|
||||
auto spawnDelay = itemManager.spawn_delay_get(character);
|
||||
auto isSpawnReady = itemManager.spawnDelayTicks <= 0;
|
||||
auto isSpawnAvailable = itemManager.spawn_available_get(character);
|
||||
auto normal_tooltip_set = [&](const char* tooltip)
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::NORMAL);
|
||||
ImGui::SetItemTooltip("%s", tooltip);
|
||||
ImGui::PopFont();
|
||||
};
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_1);
|
||||
ImGui::SeparatorText(character.data.root()->name.c_str());
|
||||
ImGui::PopFont();
|
||||
|
||||
info_content_draw(resources, character, ImVec2(ImGui::GetContentRegionAvail().x, info_height_get()));
|
||||
interact_info_content_draw(resources, character, ImVec2(ImGui::GetContentRegionAvail().x, interact_info_height_get()));
|
||||
|
||||
auto buttonHeight = ZERO_FLOAT;
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HUGE);
|
||||
buttonHeight += ImGui::GetFrameHeightWithSpacing();
|
||||
ImGui::PopFont();
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_2);
|
||||
buttonHeight += ImGui::GetFrameHeightWithSpacing();
|
||||
ImGui::PopFont();
|
||||
if (isDialogueAvailable) buttonHeight += ImGui::GetStyle().ItemSpacing.y + ONE_FLOAT;
|
||||
if (random)
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_1);
|
||||
@@ -52,12 +207,51 @@ namespace game::state::play::menu
|
||||
auto fillerHeight = ImGui::GetContentRegionAvail().y - buttonHeight;
|
||||
if (fillerHeight > ZERO_FLOAT) ImGui::Dummy(ImVec2(ImGui::GetContentRegionAvail().x, fillerHeight));
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HUGE);
|
||||
if (isSpawnReady)
|
||||
{
|
||||
ImGui::BeginDisabled(!isSpawnAvailable);
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::InteractSpawnItemButton).c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x, ZERO_FLOAT))))
|
||||
itemManager.spawn_queue(character);
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
else
|
||||
{
|
||||
auto progress = ONE_FLOAT - glm::clamp((float)itemManager.spawnDelayTicks / (float)spawnDelay, ZERO_FLOAT, ONE_FLOAT);
|
||||
ImGui::BeginDisabled();
|
||||
ImGui::ProgressBar(progress, ImVec2(ImGui::GetContentRegionAvail().x, ImGui::GetFrameHeight()),
|
||||
strings.get(Strings::InteractSpawnItemButton).c_str());
|
||||
ImGui::EndDisabled();
|
||||
std::array<char, TOOLTIP_BUFFER_SIZE> tooltip{};
|
||||
std::snprintf(tooltip.data(), tooltip.size(), strings.get(Strings::InteractSpawnItemCooldownFormat).c_str(),
|
||||
(float)itemManager.spawnDelayTicks / Entity::UPDATE_RATE);
|
||||
normal_tooltip_set(tooltip.data());
|
||||
}
|
||||
ImGui::PopFont();
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_2);
|
||||
auto autofeedLabel = autofeed.enabled_get() ? strings.get(Strings::InventoryDisableAutofeedButton)
|
||||
: strings.get(Strings::InventoryEnableAutofeedButton);
|
||||
auto isAutofeedAvailable = autofeed.available_get(character, itemManager);
|
||||
ImGui::BeginDisabled(!isAutofeedAvailable);
|
||||
if (WIDGET_FX(ImGui::Button(autofeedLabel.c_str(), ImVec2(ImGui::GetContentRegionAvail().x, ZERO_FLOAT))))
|
||||
autofeed.toggle();
|
||||
ImGui::EndDisabled();
|
||||
normal_tooltip_set(strings
|
||||
.get(autofeed.enabled_get() ? Strings::InventoryDisableAutofeedTooltip
|
||||
: Strings::InventoryEnableAutofeedTooltip)
|
||||
.c_str());
|
||||
ImGui::PopFont();
|
||||
|
||||
if (isDialogueAvailable) ImGui::Separator();
|
||||
|
||||
if (random)
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_1);
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::InteractChatButton).c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x, ZERO_FLOAT))))
|
||||
text.set(dialogue.dialogue_pool_entry_get(*random), character);
|
||||
text.set(dialogue.dialogue_hook_entry_get(Schema::Element::RANDOM), character);
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
@@ -66,7 +260,7 @@ namespace game::state::play::menu
|
||||
ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_2);
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::InteractHelpButton).c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x, ZERO_FLOAT))))
|
||||
text.set(dialogue.dialogue_entry_get(*help), character);
|
||||
text.set(dialogue.dialogue_hook_entry_get(Schema::Element::HELP), character);
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "../text.hpp"
|
||||
#include "../autofeed.hpp"
|
||||
#include "../item_manager.hpp"
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
@@ -9,6 +11,6 @@ namespace game::state::play::menu
|
||||
class Interact
|
||||
{
|
||||
public:
|
||||
void update(Resources&, Text&, Entity&, bool isMenuFullscreen);
|
||||
void update(Resources&, ItemManager&, Text&, Entity&, Autofeed&, bool isMenuFullscreen);
|
||||
};
|
||||
}
|
||||
+150
-434
@@ -1,213 +1,107 @@
|
||||
#include "inventory.hpp"
|
||||
#include "../autofeed.hpp"
|
||||
#include "../item_display.hpp"
|
||||
#include "../style.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <ranges>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
|
||||
#include "../../../resource/font.hpp"
|
||||
#include "../../../util/color.hpp"
|
||||
#include "../../../util/imgui.hpp"
|
||||
#include "../../../util/imgui/style.hpp"
|
||||
#include "../../../util/imgui/widget.hpp"
|
||||
#include "../../../util/math.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
using namespace game::util;
|
||||
using namespace game::util::imgui;
|
||||
using namespace game::resource;
|
||||
using namespace glm;
|
||||
|
||||
namespace game::state::play::menu
|
||||
{
|
||||
using Strings = resource::xml::Strings;
|
||||
|
||||
namespace
|
||||
{
|
||||
int& quantity_get(std::map<int, int>& values, const resource::xml::Schema& schema, int itemID)
|
||||
{
|
||||
auto& quantity = values[itemID];
|
||||
quantity = glm::clamp(0, quantity, schema.quantityMax);
|
||||
return quantity;
|
||||
}
|
||||
constexpr auto INFO_CHILD_HEIGHT_MAX_MULTIPLIER = 0.5f;
|
||||
constexpr auto DETAIL_CHILD_HEIGHT_MULTIPLIER = 0.5f;
|
||||
constexpr auto WINDOW_PADDING_MULTIPLIER = 2.0f;
|
||||
constexpr auto ZERO_FLOAT = 0.0f;
|
||||
constexpr auto WIDGET_MIN_SIZE = 1.0f;
|
||||
constexpr auto SELECTED_ITEM_STYLE_COLOR_COUNT = 3;
|
||||
constexpr auto TEXT_FORMAT_BUFFER_SIZE = 512;
|
||||
constexpr auto UNDISCOVERED_ITEM_TINT = 0.0f;
|
||||
constexpr auto UNDISCOVERED_ITEM_ALPHA = 0.5f;
|
||||
constexpr auto UNKNOWN_TEXT = "???";
|
||||
|
||||
bool is_possible_to_upgrade_get(const resource::xml::Schema& schema, const resource::xml::Schema::ItemEntry& item)
|
||||
{
|
||||
return item.isUpgradeID && item.isUpgradeCount && schema.idToStringMap.contains(item.upgradeID);
|
||||
}
|
||||
template <typename... Args> std::string formatted_text_get(const std::string& format, Args... args)
|
||||
{
|
||||
std::array<char, TEXT_FORMAT_BUFFER_SIZE> buffer{};
|
||||
auto size = std::snprintf(buffer.data(), buffer.size(), format.c_str(), args...);
|
||||
if (size < 0) return {};
|
||||
auto length = std::min(static_cast<std::size_t>(size), buffer.size() - 1);
|
||||
return std::string(buffer.data(), length);
|
||||
}
|
||||
|
||||
bool Inventory::can_upgrade(const resource::xml::Schema& schema, int itemID)
|
||||
float wrapped_text_height_get(std::string_view text, float wrapWidth)
|
||||
{
|
||||
if (itemID < 0 || itemID >= (int)schema.items.size()) return false;
|
||||
|
||||
auto& item = schema.items[itemID];
|
||||
auto& quantity = quantity_get(values, schema, itemID);
|
||||
return is_possible_to_upgrade_get(schema, item) && quantity >= item.upgradeCount;
|
||||
auto safeWrapWidth = std::max(WIDGET_MIN_SIZE, wrapWidth);
|
||||
return ImGui::CalcTextSize(text.data(), text.data() + text.size(), false, safeWrapWidth).y;
|
||||
}
|
||||
|
||||
bool Inventory::upgrade(resource::xml::Schema& schema, int itemID, bool isAll)
|
||||
{
|
||||
if (itemID < 0 || itemID >= (int)schema.items.size()) return false;
|
||||
|
||||
auto& item = schema.items[itemID];
|
||||
auto& quantity = quantity_get(values, schema, itemID);
|
||||
|
||||
if (!is_possible_to_upgrade_get(schema, item) || quantity < item.upgradeCount)
|
||||
{
|
||||
schema.root()->soundUpgradeFail.play();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isAll)
|
||||
{
|
||||
while (quantity >= item.upgradeCount)
|
||||
{
|
||||
values[item.upgradeID]++;
|
||||
quantity -= item.upgradeCount;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
values[item.upgradeID]++;
|
||||
quantity -= item.upgradeCount;
|
||||
}
|
||||
|
||||
schema.root()->soundUpgrade.play();
|
||||
if (quantity < item.upgradeCount && selectedItemID == itemID) selectedItemID = item.upgradeID;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Inventory::upgrade_all_possible(resource::xml::Schema& schema)
|
||||
{
|
||||
auto isUpgraded = false;
|
||||
|
||||
for (auto isPassUpgraded = true; isPassUpgraded;)
|
||||
{
|
||||
isPassUpgraded = false;
|
||||
for (int itemID = 0; itemID < (int)schema.items.size(); itemID++)
|
||||
{
|
||||
if (!can_upgrade(schema, itemID)) continue;
|
||||
isPassUpgraded = upgrade(schema, itemID, true) || isPassUpgraded;
|
||||
isUpgraded = isPassUpgraded || isUpgraded;
|
||||
}
|
||||
}
|
||||
|
||||
return isUpgraded;
|
||||
}
|
||||
|
||||
void Inventory::update(Resources& resources, ItemManager& itemManager, Entity& character, Autofeed& autofeed)
|
||||
void Inventory::update(Resources& resources, ItemManager& itemManager, Entity& character)
|
||||
{
|
||||
for (auto& [i, actor] : actors)
|
||||
actor.update();
|
||||
|
||||
static constexpr auto INFO_CHILD_HEIGHT_MAX_MULTIPLIER = 0.5f;
|
||||
bool isSelectedItemPressed{};
|
||||
int pressedItemQuantity{-1};
|
||||
|
||||
auto& schema = character.data.itemSchema;
|
||||
auto& strings = character.data.strings;
|
||||
auto isSelectedItemPressed = false;
|
||||
|
||||
auto inventory_quantity_get = [&](int itemID) -> int& { return quantity_get(values, schema, itemID); };
|
||||
|
||||
auto is_possible_to_upgrade_get = [&](const resource::xml::Schema::ItemEntry& item)
|
||||
{ return game::state::play::menu::is_possible_to_upgrade_get(schema, item); };
|
||||
|
||||
auto is_able_to_upgrade_get = [&](const resource::xml::Schema::ItemEntry& item, int quantity)
|
||||
{ return is_possible_to_upgrade_get(item) && quantity >= item.upgradeCount; };
|
||||
|
||||
auto item_header_draw = [&](const resource::xml::Schema::ItemEntry& item, int quantity)
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
ImGui::TextWrapped("%s (x%i)", item.name.c_str(), quantity);
|
||||
ImGui::PopFont();
|
||||
};
|
||||
|
||||
auto item_summary_draw = [&](const resource::xml::Schema::ItemEntry& item)
|
||||
{
|
||||
auto& category = schema.categories[item.categoryID];
|
||||
auto& rarity = schema.rarities[item.rarityID];
|
||||
auto durability = item.isDurability ? item.durability : schema.durability;
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY)));
|
||||
ImGui::TextWrapped("-- %s (%s) --", category.name.c_str(), rarity.name.c_str());
|
||||
if (item.isFlavor)
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryFlavorFormat).c_str(),
|
||||
schema.flavors[item.flavorID].name.c_str());
|
||||
if (item.isCalories) ImGui::TextWrapped(strings.get(Strings::InventoryCaloriesFormat).c_str(), item.calories);
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryDurabilityFormat).c_str(), durability);
|
||||
if (item.isCapacityBonus)
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryCapacityBonusFormat).c_str(), item.capacityBonus);
|
||||
if (item.isDigestionBonus)
|
||||
{
|
||||
if (item.digestionBonus > 0)
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryDigestionRateBonusFormat).c_str(),
|
||||
item.digestionBonus * 60.0f);
|
||||
else if (item.digestionBonus < 0)
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryDigestionRatePenaltyFormat).c_str(),
|
||||
item.digestionBonus * 60.0f);
|
||||
}
|
||||
if (item.isEatSpeedBonus)
|
||||
{
|
||||
if (item.eatSpeedBonus > 0)
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryEatSpeedBonusFormat).c_str(), item.eatSpeedBonus);
|
||||
else if (item.eatSpeedBonus < 0)
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryEatSpeedPenaltyFormat).c_str(), item.eatSpeedBonus);
|
||||
}
|
||||
if (is_possible_to_upgrade_get(item))
|
||||
ImGui::TextWrapped(strings.get(Strings::InventoryUpgradePreviewFormat).c_str(), item.upgradeCount,
|
||||
schema.idToStringMap.at(item.upgradeID).c_str());
|
||||
ImGui::PopStyleColor();
|
||||
};
|
||||
|
||||
auto info_section_separator_height_get = [&]() { return ImGui::GetStyle().ItemSpacing.y + 1.0f; };
|
||||
auto info_section_separator_height_get = []() { return ImGui::GetStyle().ItemSpacing.y + WIDGET_MIN_SIZE; };
|
||||
|
||||
auto info_section_separator_draw = [&]()
|
||||
{
|
||||
auto separatorHeight = info_section_separator_height_get();
|
||||
auto cursorScreenPos = ImGui::GetCursorScreenPos();
|
||||
auto width = ImGui::GetContentRegionAvail().x;
|
||||
auto y = cursorScreenPos.y + separatorHeight * 0.5f;
|
||||
auto y = cursorScreenPos.y + separatorHeight * DETAIL_CHILD_HEIGHT_MULTIPLIER;
|
||||
ImGui::GetWindowDrawList()->AddLine(ImVec2(cursorScreenPos.x, y), ImVec2(cursorScreenPos.x + width, y),
|
||||
ImGui::GetColorU32(ImGuiCol_Separator));
|
||||
ImGui::Dummy(ImVec2(0.0f, separatorHeight));
|
||||
ImGui::Dummy(ImVec2(ZERO_FLOAT, separatorHeight));
|
||||
};
|
||||
|
||||
auto item_tooltip_draw = [&](const resource::xml::Schema::ItemEntry& item, int quantity)
|
||||
auto item_unknown_tooltip_draw = [&]()
|
||||
{
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 24.0f);
|
||||
item_header_draw(item, quantity);
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * ITEM_TOOLTIP_WRAP_FONT_MULTIPLIER);
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
ImGui::TextWrapped("%s", UNKNOWN_TEXT);
|
||||
ImGui::PopFont();
|
||||
ImGui::Separator();
|
||||
item_summary_draw(item);
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", item.description.c_str());
|
||||
ImGui::TextWrapped("%s", UNKNOWN_TEXT);
|
||||
ImGui::PopTextWrapPos();
|
||||
};
|
||||
|
||||
auto item_unknown_draw = [&]()
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 24.0f);
|
||||
ImGui::TextWrapped("%s", strings.get(Strings::InventoryUnknown).c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::TextWrapped("%s", strings.get(Strings::InventoryEmptyHint).c_str());
|
||||
ImGui::PopFont();
|
||||
};
|
||||
|
||||
auto wrapped_text_height_get = [](std::string_view text, float wrapWidth)
|
||||
auto item_unknown_detail_draw = [&]()
|
||||
{
|
||||
auto safeWrapWidth = std::max(1.0f, wrapWidth);
|
||||
return ImGui::CalcTextSize(text.data(), text.data() + text.size(), false, safeWrapWidth).y;
|
||||
};
|
||||
|
||||
auto item_header_height_get = [&](const resource::xml::Schema::ItemEntry& item, int quantity, float width)
|
||||
{
|
||||
float height{};
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
height += wrapped_text_height_get(std::format("{} (x{})", item.name, quantity), width);
|
||||
ImGui::TextWrapped("%s", UNKNOWN_TEXT);
|
||||
ImGui::PopFont();
|
||||
info_section_separator_draw();
|
||||
ImGui::TextWrapped("%s", UNKNOWN_TEXT);
|
||||
};
|
||||
|
||||
auto item_header_height_get = [&](const resource::xml::Schema::ItemEntry& item, float width)
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
auto height = wrapped_text_height_get(item.name, width);
|
||||
ImGui::PopFont();
|
||||
return height;
|
||||
};
|
||||
|
||||
@@ -215,10 +109,9 @@ namespace game::state::play::menu
|
||||
{
|
||||
auto& category = schema.categories[item.categoryID];
|
||||
auto& rarity = schema.rarities[item.rarityID];
|
||||
auto durability = item.isDurability ? item.durability : schema.durability;
|
||||
auto itemSpacing = ImGui::GetStyle().ItemSpacing.y;
|
||||
float height{};
|
||||
int lineCount{};
|
||||
auto height = ZERO_FLOAT;
|
||||
auto lineCount = 0;
|
||||
|
||||
auto add_line_height = [&](std::string_view text)
|
||||
{
|
||||
@@ -227,75 +120,63 @@ namespace game::state::play::menu
|
||||
lineCount++;
|
||||
};
|
||||
|
||||
add_line_height(std::format("-- {} ({}) --", category.name, rarity.name));
|
||||
add_line_height(formatted_text_get("-- %s (%s) --", category.name.c_str(), rarity.name.c_str()));
|
||||
if (item.isFlavor)
|
||||
add_line_height(std::vformat(strings.get(Strings::InventoryFlavorFormat),
|
||||
std::make_format_args(schema.flavors[item.flavorID].name)));
|
||||
add_line_height(formatted_text_get(strings.get(Strings::InventoryFlavorFormat),
|
||||
schema.flavors[item.flavorID].name.c_str()));
|
||||
if (item.isCalories)
|
||||
add_line_height(
|
||||
std::vformat(strings.get(Strings::InventoryCaloriesFormat), std::make_format_args(item.calories)));
|
||||
add_line_height(std::vformat(strings.get(Strings::InventoryDurabilityFormat), std::make_format_args(durability)));
|
||||
add_line_height(formatted_text_get(strings.get(Strings::InventoryCaloriesFormat), item.calories));
|
||||
if (item.isCapacityBonus)
|
||||
add_line_height(std::vformat(strings.get(Strings::InventoryCapacityBonusFormat),
|
||||
std::make_format_args(item.capacityBonus)));
|
||||
add_line_height(formatted_text_get(strings.get(Strings::InventoryCapacityBonusFormat), item.capacityBonus));
|
||||
if (item.isDigestionBonus)
|
||||
{
|
||||
if (item.digestionBonus > 0)
|
||||
{
|
||||
auto digestionRateBonus = item.digestionBonus * 60.0f;
|
||||
add_line_height(std::vformat(strings.get(Strings::InventoryDigestionRateBonusFormat),
|
||||
std::make_format_args(digestionRateBonus)));
|
||||
}
|
||||
else if (item.digestionBonus < 0)
|
||||
{
|
||||
auto digestionRatePenalty = item.digestionBonus * 60.0f;
|
||||
add_line_height(std::vformat(strings.get(Strings::InventoryDigestionRatePenaltyFormat),
|
||||
std::make_format_args(digestionRatePenalty)));
|
||||
}
|
||||
if (item.digestionBonus > ZERO_FLOAT)
|
||||
add_line_height(formatted_text_get(strings.get(Strings::InventoryDigestionRateBonusFormat),
|
||||
item.digestionBonus * Entity::UPDATE_RATE));
|
||||
else if (item.digestionBonus < ZERO_FLOAT)
|
||||
add_line_height(formatted_text_get(strings.get(Strings::InventoryDigestionRatePenaltyFormat),
|
||||
item.digestionBonus * Entity::UPDATE_RATE));
|
||||
}
|
||||
if (item.isEatSpeedBonus)
|
||||
{
|
||||
if (item.eatSpeedBonus > 0)
|
||||
add_line_height(std::vformat(strings.get(Strings::InventoryEatSpeedBonusFormat),
|
||||
std::make_format_args(item.eatSpeedBonus)));
|
||||
else if (item.eatSpeedBonus < 0)
|
||||
add_line_height(std::vformat(strings.get(Strings::InventoryEatSpeedPenaltyFormat),
|
||||
std::make_format_args(item.eatSpeedBonus)));
|
||||
}
|
||||
if (is_possible_to_upgrade_get(item))
|
||||
add_line_height(
|
||||
std::vformat(strings.get(Strings::InventoryUpgradePreviewFormat),
|
||||
std::make_format_args(item.upgradeCount, schema.idToStringMap.at(item.upgradeID))));
|
||||
return height;
|
||||
};
|
||||
|
||||
auto item_details_height_get = [&](const resource::xml::Schema::ItemEntry& item, int quantity, float width)
|
||||
auto item_details_height_get = [&](const resource::xml::Schema::ItemEntry& item, float width)
|
||||
{
|
||||
auto separatorHeight = info_section_separator_height_get();
|
||||
auto detailBodyHeight =
|
||||
std::max(item_summary_height_get(item, width), wrapped_text_height_get(item.description, width));
|
||||
return item_header_height_get(item, quantity, width) + separatorHeight + detailBodyHeight * 2.0f +
|
||||
return item_header_height_get(item, width) + separatorHeight + detailBodyHeight * WINDOW_PADDING_MULTIPLIER +
|
||||
separatorHeight;
|
||||
};
|
||||
|
||||
auto item_unknown_height_get = [&](float width)
|
||||
auto item_unknown_details_height_get = [&](float width)
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
auto height = wrapped_text_height_get(strings.get(Strings::InventoryUnknown), width);
|
||||
auto headerHeight = wrapped_text_height_get(UNKNOWN_TEXT, width);
|
||||
ImGui::PopFont();
|
||||
return height;
|
||||
auto separatorHeight = info_section_separator_height_get();
|
||||
return headerHeight + separatorHeight + wrapped_text_height_get(UNKNOWN_TEXT, width);
|
||||
};
|
||||
|
||||
auto info_child_height_get = [&](ImVec2 available, bool isItemSelected, int selectedQuantity)
|
||||
auto item_discovered_get = [&](int itemID)
|
||||
{
|
||||
auto isInfoVisible = isItemSelected || count() == 0;
|
||||
if (!isInfoVisible) return 0.0f;
|
||||
return itemID >= 0 && itemID < (int)itemManager.discoveredItemIDs.size() && itemManager.discoveredItemIDs[itemID];
|
||||
};
|
||||
|
||||
auto infoWidth = std::max(1.0f, available.x - ImGui::GetStyle().WindowPadding.x * 2.0f);
|
||||
auto infoPadding = ImGui::GetStyle().WindowPadding.y * 2.0f;
|
||||
auto item_catalog_visible_get = [&](int itemID)
|
||||
{
|
||||
return itemID >= 0 && itemID < (int)schema.items.size();
|
||||
};
|
||||
|
||||
auto info_child_height_get = [&](ImVec2 available, bool isItemSelected)
|
||||
{
|
||||
if (!isItemSelected && !schema.items.empty()) return ZERO_FLOAT;
|
||||
|
||||
auto infoWidth =
|
||||
std::max(WIDGET_MIN_SIZE, available.x - ImGui::GetStyle().WindowPadding.x * WINDOW_PADDING_MULTIPLIER);
|
||||
auto infoPadding = ImGui::GetStyle().WindowPadding.y * WINDOW_PADDING_MULTIPLIER;
|
||||
auto separatorHeight = info_section_separator_height_get();
|
||||
auto measuredBodyHeight = 0.0f;
|
||||
auto isSelectedItemKnown = isItemSelected && selectedQuantity > 0;
|
||||
auto measuredBodyHeight = ZERO_FLOAT;
|
||||
|
||||
if (!isItemSelected)
|
||||
{
|
||||
@@ -306,112 +187,57 @@ namespace game::state::play::menu
|
||||
else
|
||||
{
|
||||
auto& item = schema.items[selectedItemID];
|
||||
measuredBodyHeight = isSelectedItemKnown ? item_details_height_get(item, selectedQuantity, infoWidth)
|
||||
: item_unknown_height_get(infoWidth);
|
||||
measuredBodyHeight = item_discovered_get(selectedItemID) ? item_details_height_get(item, infoWidth)
|
||||
: item_unknown_details_height_get(infoWidth);
|
||||
}
|
||||
|
||||
auto buttonChildHeight = 0.0f;
|
||||
if (selectedQuantity > 0)
|
||||
{
|
||||
ImGui::PushFont(resources.font.get(), Font::HEADER_2);
|
||||
auto buttonRowHeight = ImGui::GetFrameHeight();
|
||||
buttonChildHeight = buttonRowHeight * 2.0f + ImGui::GetStyle().ItemSpacing.y * 5.0f;
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
auto desiredInfoChildHeight = infoPadding + separatorHeight + measuredBodyHeight + buttonChildHeight +
|
||||
(buttonChildHeight > 0.0f ? separatorHeight : 0.0f);
|
||||
auto desiredInfoChildHeight = infoPadding + separatorHeight + measuredBodyHeight;
|
||||
auto maxInfoChildHeight = available.y * INFO_CHILD_HEIGHT_MAX_MULTIPLIER;
|
||||
return glm::clamp(desiredInfoChildHeight, 0.0f, maxInfoChildHeight);
|
||||
};
|
||||
|
||||
auto item_use = [&](int itemID)
|
||||
{
|
||||
auto& item = schema.items[itemID];
|
||||
auto& category = schema.categories[item.categoryID];
|
||||
auto& quantity = inventory_quantity_get(itemID);
|
||||
|
||||
if (quantity <= 0) return;
|
||||
|
||||
if (category.isEdible)
|
||||
{
|
||||
if (itemManager.items.size() + 1 >= ItemManager::LIMIT)
|
||||
character.data.itemSchema.root()->soundDispose.play();
|
||||
else
|
||||
{
|
||||
character.data.itemSchema.root()->soundSummon.play();
|
||||
itemManager.queuedItemIDs.emplace_back(itemID);
|
||||
quantity--;
|
||||
if (quantity <= 0) selectedItemID = -1;
|
||||
}
|
||||
}
|
||||
else if (item.isToggleSpritesheet)
|
||||
{
|
||||
character.spritesheet_set(character.spritesheetType == Entity::NORMAL ? Entity::ALTERNATE : Entity::NORMAL);
|
||||
if (auto* alternate = character.data.alternate_spritesheet()) alternate->soundEntry.sound.play();
|
||||
quantity--;
|
||||
}
|
||||
return std::clamp(desiredInfoChildHeight, ZERO_FLOAT, maxInfoChildHeight);
|
||||
};
|
||||
|
||||
auto item_actor_get = [&](int itemID) -> Entity&
|
||||
{
|
||||
if (!actors.contains(itemID)) actors[itemID] = Entity(schema.anm2s[itemID], {}, Entity::SET);
|
||||
|
||||
return actors[itemID];
|
||||
};
|
||||
|
||||
if (!itemManager.returnItemIDs.empty())
|
||||
{
|
||||
for (auto& id : itemManager.returnItemIDs)
|
||||
values[id]++;
|
||||
itemManager.returnItemIDs.clear();
|
||||
}
|
||||
|
||||
if (ImGui::BeginChild("##Inventory Child", ImGui::GetContentRegionAvail(), ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar))
|
||||
{
|
||||
auto inventoryCount = count();
|
||||
auto available = ImGui::GetContentRegionAvail();
|
||||
auto isItemSelected = selectedItemID >= 0 && selectedItemID < (int)schema.items.size();
|
||||
auto isInfoVisible = isItemSelected || inventoryCount == 0;
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
auto toggleButtonHeight = ImGui::GetFrameHeight();
|
||||
ImGui::PopFont();
|
||||
auto toggleChildHeight = toggleButtonHeight + ImGui::GetStyle().WindowPadding.y * 2.0f;
|
||||
auto isItemSelected =
|
||||
selectedItemID >= 0 && selectedItemID < (int)schema.items.size() && item_catalog_visible_get(selectedItemID);
|
||||
auto isInfoVisible = isItemSelected || schema.items.empty();
|
||||
auto toggleSpacing = ImGui::GetStyle().ItemSpacing.y;
|
||||
auto infoChildHeight =
|
||||
info_child_height_get(available, isItemSelected, isItemSelected ? inventory_quantity_get(selectedItemID) : 0);
|
||||
auto infoChildHeight = info_child_height_get(available, isItemSelected);
|
||||
|
||||
auto inventoryChildHeight = std::max(0.0f, available.y - infoChildHeight - toggleChildHeight -
|
||||
(isInfoVisible ? toggleSpacing * 2.0f : toggleSpacing));
|
||||
auto inventoryChildHeight =
|
||||
std::max(ZERO_FLOAT, available.y - infoChildHeight -
|
||||
(isInfoVisible ? toggleSpacing * WINDOW_PADDING_MULTIPLIER : toggleSpacing));
|
||||
auto childSize = ImVec2(available.x, inventoryChildHeight);
|
||||
auto toggleChildSize = ImVec2(available.x, toggleChildHeight);
|
||||
auto infoChildSize = ImVec2(available.x, infoChildHeight);
|
||||
|
||||
if (ImGui::BeginChild("##Inventory List Child", childSize))
|
||||
{
|
||||
auto cursorPos = ImGui::GetCursorPos();
|
||||
auto cursorStartX = ImGui::GetCursorPosX();
|
||||
bool isAnyInventoryItemHovered{};
|
||||
|
||||
auto isAnyInventoryItemHovered = false;
|
||||
auto size = ImVec2(SIZE, SIZE);
|
||||
|
||||
for (int i = 0; i < (int)schema.items.size(); i++)
|
||||
{
|
||||
auto& item = schema.items[i];
|
||||
auto& quantity = inventory_quantity_get(i);
|
||||
auto& rarity = schema.rarities[item.rarityID];
|
||||
if (!item_catalog_visible_get(i)) continue;
|
||||
|
||||
auto isItemColor = item.isColor;
|
||||
|
||||
if (rarity.isHidden && quantity <= 0) continue;
|
||||
|
||||
ImGui::PushID(i);
|
||||
|
||||
ImGui::SetCursorPos(cursorPos);
|
||||
auto cursorScreenPos = ImGui::GetCursorScreenPos();
|
||||
auto& actor = item_actor_get(i);
|
||||
auto isSelected = selectedItemID == i;
|
||||
if (isItemColor) imgui::style::color_set(item.color);
|
||||
auto isDiscovered = item_discovered_get(i);
|
||||
if (isItemColor && isDiscovered) imgui::style::color_set(item.color);
|
||||
|
||||
if (isSelected)
|
||||
{
|
||||
@@ -421,35 +247,26 @@ namespace game::state::play::menu
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, selectedColor);
|
||||
}
|
||||
|
||||
auto isPressed = itemButtons[i].button(resources, actor, "##Image Button", size,
|
||||
quantity <= 0 ? ImVec4(0, 0, 0, 0.5f) : ImVec4(1, 1, 1, 1));
|
||||
if (isSelected) ImGui::PopStyleColor(3);
|
||||
auto tint = isDiscovered ? ImVec4(1, 1, 1, 1)
|
||||
: ImVec4(UNDISCOVERED_ITEM_TINT, UNDISCOVERED_ITEM_TINT, UNDISCOVERED_ITEM_TINT,
|
||||
UNDISCOVERED_ITEM_ALPHA);
|
||||
auto isPressed = itemButtons[i].button(resources, actor, "##Image Button", size, tint);
|
||||
if (isSelected) ImGui::PopStyleColor(SELECTED_ITEM_STYLE_COLOR_COUNT);
|
||||
isAnyInventoryItemHovered = isAnyInventoryItemHovered || ImGui::IsItemHovered();
|
||||
if (isPressed)
|
||||
{
|
||||
isSelectedItemPressed = selectedItemID != i;
|
||||
selectedItemID = i;
|
||||
pressedItemQuantity = quantity;
|
||||
}
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && quantity > 0) item_use(i);
|
||||
if (!isSelected && ImGui::BeginItemTooltip())
|
||||
{
|
||||
if (quantity > 0)
|
||||
item_tooltip_draw(item, quantity);
|
||||
if (isDiscovered)
|
||||
item_tooltip_draw(character, schema, item);
|
||||
else
|
||||
item_unknown_draw();
|
||||
item_unknown_tooltip_draw();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
|
||||
auto text = std::format("x{}", quantity);
|
||||
auto textPos = ImVec2(cursorScreenPos.x + size.x - ImGui::CalcTextSize(text.c_str()).x,
|
||||
cursorScreenPos.y + size.y - ImGui::GetTextLineHeightWithSpacing());
|
||||
ImGui::GetWindowDrawList()->AddText(textPos, ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Text)),
|
||||
text.c_str());
|
||||
ImGui::PopFont();
|
||||
if (isItemColor) style::color_set(resources, character);
|
||||
if (isItemColor && isDiscovered) style::color_set(resources, character);
|
||||
|
||||
auto increment = ImGui::GetItemRectSize().x + ImGui::GetStyle().ItemSpacing.x;
|
||||
cursorPos.x += increment;
|
||||
@@ -468,177 +285,76 @@ namespace game::state::play::menu
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
if (ImGui::BeginChild("##Autofeed Toggle Child", toggleChildSize, ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse))
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
auto isAutofeedAvailable = autofeed.available_get(character, *this, itemManager);
|
||||
auto label = autofeed.enabled_get() ? strings.get(Strings::InventoryDisableAutofeedButton)
|
||||
: strings.get(Strings::InventoryEnableAutofeedButton);
|
||||
ImGui::BeginDisabled(!isAutofeedAvailable);
|
||||
if (WIDGET_FX(ImGui::Button(label.c_str(), {ImGui::GetContentRegionAvail().x, 0}))) autofeed.toggle();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopFont();
|
||||
ImGui::SetItemTooltip("%s", strings
|
||||
.get(autofeed.enabled_get() ? Strings::InventoryDisableAutofeedTooltip
|
||||
: Strings::InventoryEnableAutofeedTooltip)
|
||||
.c_str());
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
isItemSelected = selectedItemID >= 0 && selectedItemID < (int)schema.items.size();
|
||||
isInfoVisible = isItemSelected || inventoryCount == 0;
|
||||
auto selectedQuantity =
|
||||
isItemSelected ? (isSelectedItemPressed && pressedItemQuantity >= 0 ? pressedItemQuantity
|
||||
: inventory_quantity_get(selectedItemID))
|
||||
: 0;
|
||||
infoChildHeight = info_child_height_get(available, isItemSelected, selectedQuantity);
|
||||
isItemSelected =
|
||||
selectedItemID >= 0 && selectedItemID < (int)schema.items.size() && item_catalog_visible_get(selectedItemID);
|
||||
isInfoVisible = isItemSelected || schema.items.empty();
|
||||
infoChildHeight = info_child_height_get(available, isItemSelected);
|
||||
infoChildSize = ImVec2(available.x, infoChildHeight);
|
||||
auto isSelectedItemKnown = isItemSelected && selectedQuantity > 0;
|
||||
auto selectedItemHasColor = isItemSelected && schema.items[selectedItemID].isColor;
|
||||
auto isSelectedItemDiscovered = isItemSelected && item_discovered_get(selectedItemID);
|
||||
auto selectedItemHasColor = isSelectedItemDiscovered && schema.items[selectedItemID].isColor;
|
||||
|
||||
if (isInfoVisible && ImGui::BeginChild("##Info Child", infoChildSize, ImGuiChildFlags_None,
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse))
|
||||
{
|
||||
if (selectedItemHasColor) imgui::style::color_set(schema.items[selectedItemID].color);
|
||||
info_section_separator_draw();
|
||||
auto isButtonChildVisible = selectedQuantity > 0;
|
||||
ImGui::PushFont(resources.font.get(), Font::HEADER_2);
|
||||
auto buttonRowHeight = ImGui::GetFrameHeight();
|
||||
auto buttonChildHeight =
|
||||
isButtonChildVisible ? buttonRowHeight * 2.0f + ImGui::GetStyle().ItemSpacing.y * 5.0f : 0.0f;
|
||||
ImGui::PopFont();
|
||||
auto separatorHeight = info_section_separator_height_get();
|
||||
|
||||
if (!isItemSelected)
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
ImGui::TextWrapped("%s", strings.get(Strings::InventoryEmptyHint).c_str());
|
||||
ImGui::PopFont();
|
||||
item_unknown_draw();
|
||||
}
|
||||
else if (isSelectedItemKnown)
|
||||
else
|
||||
{
|
||||
auto& selectedItem = schema.items[selectedItemID];
|
||||
auto contentWidth = std::max(1.0f, ImGui::GetContentRegionAvail().x);
|
||||
auto statsHeight = item_summary_height_get(selectedItem, contentWidth);
|
||||
auto descriptionHeight = wrapped_text_height_get(selectedItem.description, contentWidth);
|
||||
auto sharedDetailHeight = std::max(statsHeight, descriptionHeight);
|
||||
auto headerHeight = item_header_height_get(selectedItem, selectedQuantity, contentWidth);
|
||||
auto desiredInfoContentHeight = headerHeight + separatorHeight + sharedDetailHeight * 2.0f + separatorHeight;
|
||||
auto availableInfoContentHeight =
|
||||
std::max(0.0f, ImGui::GetContentRegionAvail().y -
|
||||
(isButtonChildVisible ? separatorHeight + buttonChildHeight : 0.0f));
|
||||
auto infoContentHeight = std::min(desiredInfoContentHeight, availableInfoContentHeight);
|
||||
|
||||
item_header_draw(selectedItem, selectedQuantity);
|
||||
info_section_separator_draw();
|
||||
|
||||
auto detailChildHeight = sharedDetailHeight;
|
||||
if (desiredInfoContentHeight > availableInfoContentHeight)
|
||||
detailChildHeight =
|
||||
std::max(0.0f, (infoContentHeight - headerHeight - separatorHeight - separatorHeight) * 0.5f);
|
||||
auto detailChildSize = ImVec2(ImGui::GetContentRegionAvail().x, detailChildHeight);
|
||||
|
||||
if (ImGui::BeginChild("##Info Stats Child", detailChildSize))
|
||||
if (!isSelectedItemDiscovered)
|
||||
{
|
||||
if (isSelectedItemPressed) ImGui::SetScrollY(0.0f);
|
||||
item_summary_draw(selectedItem);
|
||||
item_unknown_detail_draw();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
info_section_separator_draw();
|
||||
|
||||
if (ImGui::BeginChild("##Info Description Child", detailChildSize))
|
||||
else
|
||||
{
|
||||
if (isSelectedItemPressed) ImGui::SetScrollY(0.0f);
|
||||
ImGui::TextWrapped("%s", selectedItem.description.c_str());
|
||||
}
|
||||
ImGui::EndChild();
|
||||
auto contentWidth = std::max(WIDGET_MIN_SIZE, ImGui::GetContentRegionAvail().x);
|
||||
auto statsHeight = item_summary_height_get(selectedItem, contentWidth);
|
||||
auto descriptionHeight = wrapped_text_height_get(selectedItem.description, contentWidth);
|
||||
auto sharedDetailHeight = std::max(statsHeight, descriptionHeight);
|
||||
auto headerHeight = item_header_height_get(selectedItem, contentWidth);
|
||||
auto separatorHeight = info_section_separator_height_get();
|
||||
auto desiredInfoContentHeight =
|
||||
headerHeight + separatorHeight + sharedDetailHeight * WINDOW_PADDING_MULTIPLIER + separatorHeight;
|
||||
auto availableInfoContentHeight = std::max(ZERO_FLOAT, ImGui::GetContentRegionAvail().y);
|
||||
auto infoContentHeight = std::min(desiredInfoContentHeight, availableInfoContentHeight);
|
||||
|
||||
if (isButtonChildVisible)
|
||||
{
|
||||
item_header_draw(selectedItem);
|
||||
info_section_separator_draw();
|
||||
|
||||
if (ImGui::BeginChild("##Info Actions Child", ImVec2(ImGui::GetContentRegionAvail().x, buttonChildHeight),
|
||||
ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar))
|
||||
auto detailChildHeight = sharedDetailHeight;
|
||||
if (desiredInfoContentHeight > availableInfoContentHeight)
|
||||
detailChildHeight =
|
||||
std::max(ZERO_FLOAT, (infoContentHeight - headerHeight - separatorHeight - separatorHeight) *
|
||||
DETAIL_CHILD_HEIGHT_MULTIPLIER);
|
||||
auto detailChildSize = ImVec2(ImGui::GetContentRegionAvail().x, detailChildHeight);
|
||||
|
||||
if (ImGui::BeginChild("##Info Stats Child", detailChildSize))
|
||||
{
|
||||
auto canUseSelectedItem = true;
|
||||
auto canUpgradeSelectedItem = is_able_to_upgrade_get(selectedItem, selectedQuantity);
|
||||
auto rowTwoButtonSize = row_widget_size_get(2);
|
||||
if (isSelectedItemPressed) ImGui::SetScrollY(ZERO_FLOAT);
|
||||
item_summary_draw(character, schema, selectedItem);
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
auto upgrade_item_name_get = [&]() -> std::string
|
||||
{
|
||||
if (!selectedItem.isUpgradeID) return {};
|
||||
return schema.items.at(selectedItem.upgradeID).name;
|
||||
};
|
||||
info_section_separator_draw();
|
||||
|
||||
auto upgrade_tooltip_get = [&](bool isAll)
|
||||
{
|
||||
if (!is_possible_to_upgrade_get(selectedItem)) return strings.get(Strings::InventoryUpgradeNoPath);
|
||||
|
||||
auto upgradeItemName = upgrade_item_name_get();
|
||||
auto upgradeCount = selectedItem.upgradeCount;
|
||||
|
||||
if (!canUpgradeSelectedItem)
|
||||
return std::vformat(strings.get(Strings::InventoryUpgradeNeedsTemplate),
|
||||
std::make_format_args(upgradeCount, upgradeItemName));
|
||||
|
||||
if (!isAll)
|
||||
return std::vformat(strings.get(Strings::InventoryUpgradeOneTemplate),
|
||||
std::make_format_args(upgradeCount, upgradeItemName));
|
||||
|
||||
auto upgradedCount = selectedQuantity / upgradeCount;
|
||||
return std::vformat(strings.get(Strings::InventoryUpgradeAllTemplate),
|
||||
std::make_format_args(upgradeCount, upgradedCount, upgradeItemName));
|
||||
};
|
||||
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
|
||||
ImGui::BeginDisabled(!canUseSelectedItem);
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::InventorySpawnButton).c_str(),
|
||||
{ImGui::GetContentRegionAvail().x, 0})))
|
||||
item_use(selectedItemID);
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::BeginDisabled(!canUpgradeSelectedItem);
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::InventoryUpgradeButton).c_str(), rowTwoButtonSize)))
|
||||
upgrade(schema, selectedItemID, false);
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::NORMAL);
|
||||
ImGui::SetItemTooltip("%s", upgrade_tooltip_get(false).c_str());
|
||||
ImGui::PopFont();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (WIDGET_FX(ImGui::Button(strings.get(Strings::InventoryUpgradeAllButton).c_str(), rowTwoButtonSize)))
|
||||
upgrade(schema, selectedItemID, true);
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
{
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::NORMAL);
|
||||
ImGui::SetItemTooltip("%s", upgrade_tooltip_get(true).c_str());
|
||||
ImGui::PopFont();
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::PopFont();
|
||||
if (ImGui::BeginChild("##Info Description Child", detailChildSize))
|
||||
{
|
||||
if (isSelectedItemPressed) ImGui::SetScrollY(ZERO_FLOAT);
|
||||
ImGui::TextWrapped("%s", selectedItem.description.c_str());
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
}
|
||||
else
|
||||
item_unknown_draw();
|
||||
|
||||
if (selectedItemHasColor) style::color_set(resources, character);
|
||||
}
|
||||
if (isInfoVisible) ImGui::EndChild();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
int Inventory::count()
|
||||
{
|
||||
int count{};
|
||||
for (auto& [type, quantity] : values)
|
||||
count += quantity;
|
||||
return count;
|
||||
}
|
||||
}
|
||||
@@ -22,15 +22,10 @@ namespace game::state::play::menu
|
||||
public:
|
||||
static constexpr auto SIZE = 96.0f;
|
||||
|
||||
std::map<int, int> values{};
|
||||
std::unordered_map<int, Entity> actors{};
|
||||
std::unordered_map<int, util::imgui::widget::EntityButton> itemButtons{};
|
||||
int selectedItemID{-1};
|
||||
|
||||
bool can_upgrade(const resource::xml::Schema&, int itemID);
|
||||
bool upgrade(resource::xml::Schema&, int itemID, bool isAll);
|
||||
bool upgrade_all_possible(resource::xml::Schema&);
|
||||
void update(Resources&, ItemManager&, Entity&, Autofeed&);
|
||||
int count();
|
||||
void update(Resources&, ItemManager&, Entity&);
|
||||
};
|
||||
}
|
||||
+74
-12
@@ -24,6 +24,30 @@ namespace game::state::play
|
||||
return element.type == resource::xml::Schema::Element::EFFECT;
|
||||
}
|
||||
|
||||
bool dialogue_scene_effect_get(const resource::xml::Schema& dialogue, const resource::xml::Schema::Element& entry)
|
||||
{
|
||||
for (auto child : entry.children)
|
||||
{
|
||||
if (child < 0 || child >= (int)dialogue.elements.size()) continue;
|
||||
auto& effect = dialogue.elements[child];
|
||||
if (dialogue_effect_is(effect) && effect.typeString == "Scene") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool dialogue_silent_effect_get(const resource::xml::Schema& dialogue, const resource::xml::Schema::Element& entry,
|
||||
int index)
|
||||
{
|
||||
for (auto child : entry.children)
|
||||
{
|
||||
if (child < 0 || child >= (int)dialogue.elements.size()) continue;
|
||||
auto& effect = dialogue.elements[child];
|
||||
if (dialogue_effect_is(effect) && effect.typeString == "Silent" && effect.start <= index && index < effect.end)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr auto OSCILLATION_AMPLITUDE_DEFAULT = 2.0f;
|
||||
constexpr auto OSCILLATION_FREQUENCY_DEFAULT = 1.0f;
|
||||
constexpr auto SCREEN_SHAKE_MAGNITUDE_DEFAULT = 0.03f;
|
||||
@@ -95,11 +119,15 @@ namespace game::state::play
|
||||
}
|
||||
}
|
||||
|
||||
void Text::set(resource::xml::Schema::Element* dialogueEntry, Entity& character, bool isInterruptible)
|
||||
void Text::set(resource::xml::Schema::Element* dialogueEntry, Entity& character, bool isInterruptible,
|
||||
bool isSequenceAdvance)
|
||||
{
|
||||
if (!dialogueEntry) return;
|
||||
this->entry = dialogueEntry;
|
||||
|
||||
if (!isSequenceAdvance) isSceneActive = false;
|
||||
if (!isSceneActive && !isSequenceAdvance)
|
||||
isSceneActive = dialogue_scene_effect_get(character.data.dialogue, *dialogueEntry);
|
||||
isFinished = false;
|
||||
isTerminal = dialogue_entry_terminal_get(character.data.dialogue, *dialogueEntry);
|
||||
index = 0;
|
||||
@@ -108,13 +136,19 @@ namespace game::state::play
|
||||
currentDelay = 0;
|
||||
isStartEffectsApplied = false;
|
||||
isBlipQueued = false;
|
||||
isAnimationEntryStarted = false;
|
||||
animationEntryTimePrevious = 0.0f;
|
||||
time = 0.0f;
|
||||
isEnabled = true;
|
||||
if (!dialogueEntry->animation.empty())
|
||||
{
|
||||
auto interrupt = isSequenceAdvance ? Entity::Interrupt::ALWAYS
|
||||
: isInterruptible ? Entity::Interrupt::IF_ALLOWED
|
||||
: Entity::Interrupt::NEVER;
|
||||
character.play({.animation = dialogueEntry->animation,
|
||||
.appendID = character.animation_append_id_get(),
|
||||
.interrupt = isInterruptible ? Entity::Interrupt::IF_ALLOWED
|
||||
: Entity::Interrupt::NEVER});
|
||||
.appendID = character.animation_append_id_get(),
|
||||
.interrupt = interrupt});
|
||||
}
|
||||
auto* dialogueRoot = character.data.dialogue.root();
|
||||
baseDelay = dialogueRoot && dialogueRoot->delayTicks > 0 ? dialogueRoot->delayTicks : 2;
|
||||
currentDelay = baseDelay;
|
||||
@@ -135,10 +169,10 @@ namespace game::state::play
|
||||
.mode = Entity::PLAY_FORCE});
|
||||
}
|
||||
}
|
||||
if (dialogueEntry->text.empty())
|
||||
if (dialogueEntry->text.empty() && dialogueEntry->next.empty())
|
||||
isEnabled = false;
|
||||
else
|
||||
character.isTalking = true;
|
||||
else if (!dialogueEntry->text.empty())
|
||||
character.isTalking = !dialogue_silent_effect_get(character.data.dialogue, *dialogueEntry, 0);
|
||||
}
|
||||
|
||||
void Text::update(Entity& character, Canvas& canvas)
|
||||
@@ -191,11 +225,14 @@ namespace game::state::play
|
||||
}
|
||||
}
|
||||
auto delayCharacter = dialogue_delay_character_get(character.data.dialogue, entry->text, index - 1, currentDelay);
|
||||
auto isSilent = dialogue_silent_effect_get(character.data.dialogue, *entry, index - 1);
|
||||
currentDelay = delayCharacter.delay;
|
||||
character.isTalking = !delayCharacter.isDisableTalk;
|
||||
character.isTalking = !delayCharacter.isDisableTalk && !isSilent;
|
||||
auto blipDelay = dialogueRoot ? dialogueRoot->blipDelayTicks : 0;
|
||||
auto isBlipStep = blipDelay > 0 && index % blipDelay == 0;
|
||||
if (delayCharacter.isDisableTalk)
|
||||
if (isSilent)
|
||||
isBlipQueued = false;
|
||||
else if (delayCharacter.isDisableTalk)
|
||||
{
|
||||
if (isBlipStep) isBlipQueued = true;
|
||||
}
|
||||
@@ -210,10 +247,31 @@ namespace game::state::play
|
||||
isFinished = true;
|
||||
character.isTalking = false;
|
||||
isBlipQueued = false;
|
||||
if (isTerminal) isSceneActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entry && entry->text.empty())
|
||||
{
|
||||
auto* animation = character.animation_get();
|
||||
auto isAnimationEntryReady = entry->animation.empty() || !animation || character.state == Entity::STOPPED ||
|
||||
(!isAnimationEntryStarted && animation->frameNum <= 1) ||
|
||||
(animation->isLoop && isAnimationEntryStarted &&
|
||||
character.time < animationEntryTimePrevious);
|
||||
isAnimationEntryStarted = true;
|
||||
animationEntryTimePrevious = character.time;
|
||||
if (!entry->next.empty() && isAnimationEntryReady)
|
||||
set(character.data.dialogue.dialogue_entry_get(*entry), character, true, true);
|
||||
else if (entry->next.empty())
|
||||
{
|
||||
isFinished = true;
|
||||
isEnabled = false;
|
||||
if (isTerminal) isSceneActive = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto& dialogue = character.data.dialogue;
|
||||
auto& menuSchema = character.data.menuSchema;
|
||||
|
||||
@@ -326,7 +384,7 @@ namespace game::state::play
|
||||
auto label = branch->text.empty() ? "<choice>" : branch->text.c_str();
|
||||
ImGui::PushID(index);
|
||||
if (WIDGET_FX(ImGui::Button(label, buttonSize)))
|
||||
set(dialogue.dialogue_entry_get(*branch), character);
|
||||
set(dialogue.dialogue_entry_get(*branch), character, true, true);
|
||||
|
||||
ImGui::SetItemTooltip("%s", label);
|
||||
ImGui::PopID();
|
||||
@@ -337,7 +395,7 @@ namespace game::state::play
|
||||
{
|
||||
auto* next = dialogue.dialogue_entry_get(*choices.front());
|
||||
if (next) menuSchema.root()->soundSelect.play();
|
||||
set(next, character);
|
||||
set(next, character, true, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -364,13 +422,14 @@ namespace game::state::play
|
||||
{
|
||||
auto* next = dialogue.dialogue_entry_get(*entry);
|
||||
if (next) menuSchema.root()->soundSelect.play();
|
||||
set(next, character);
|
||||
set(next, character, true, true);
|
||||
}
|
||||
}
|
||||
else if (isAdvance)
|
||||
{
|
||||
isEnabled = false;
|
||||
entry = nullptr;
|
||||
isSceneActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -406,6 +465,7 @@ namespace game::state::play
|
||||
index = target;
|
||||
isFinished = true;
|
||||
character.isTalking = false;
|
||||
if (isTerminal) isSceneActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,6 +481,7 @@ namespace game::state::play
|
||||
{
|
||||
isEnabled = false;
|
||||
entry = nullptr;
|
||||
isSceneActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -428,4 +489,5 @@ namespace game::state::play
|
||||
bool Text::is_interruptible() const { return !entry || isTerminal; }
|
||||
bool Text::is_finished() const { return isFinished; }
|
||||
bool Text::is_terminal() const { return isTerminal; }
|
||||
bool Text::is_scene_active() const { return isSceneActive; }
|
||||
}
|
||||
@@ -19,6 +19,9 @@ namespace game::state::play
|
||||
bool isTerminal{};
|
||||
bool isStartEffectsApplied{};
|
||||
bool isBlipQueued{};
|
||||
bool isSceneActive{};
|
||||
bool isAnimationEntryStarted{};
|
||||
float animationEntryTimePrevious{};
|
||||
|
||||
public:
|
||||
static constexpr auto LIFETIME = 10.0f;
|
||||
@@ -28,10 +31,11 @@ namespace game::state::play
|
||||
bool isEnabled{true};
|
||||
float time{};
|
||||
|
||||
void set(resource::xml::Schema::Element*, Entity&, bool isInterruptible = true);
|
||||
void set(resource::xml::Schema::Element*, Entity&, bool isInterruptible = true, bool isSequenceAdvance = false);
|
||||
void update(Entity&, Canvas&);
|
||||
bool is_interruptible() const;
|
||||
bool is_finished() const;
|
||||
bool is_terminal() const;
|
||||
bool is_scene_active() const;
|
||||
};
|
||||
}
|
||||
+35
-38
@@ -10,37 +10,40 @@ using namespace game::util;
|
||||
|
||||
namespace game::state::play
|
||||
{
|
||||
constexpr auto ZOOM_LADDER_EPSILON = 0.01f;
|
||||
constexpr auto ZOOM_WHEEL_ZERO = 0.0f;
|
||||
constexpr auto ZERO_FLOAT = 0.0f;
|
||||
constexpr auto ONE_INT = 1;
|
||||
|
||||
float zoom_ladder_level_get(int index)
|
||||
int zoom_ladder_index_get(float zoom)
|
||||
{
|
||||
auto clampedIndex = std::clamp(index, 0, World::ZOOM_LADDER_LEVEL_COUNT - 1);
|
||||
auto exponent = (float)clampedIndex / (float)(World::ZOOM_LADDER_LEVEL_COUNT - 1);
|
||||
return World::ZOOM_MIN * (float)std::pow(World::ZOOM_MAX / World::ZOOM_MIN, exponent);
|
||||
}
|
||||
|
||||
int zoom_ladder_next_index_get(float zoom, int direction)
|
||||
{
|
||||
if (direction > 0)
|
||||
auto result = 0;
|
||||
auto distance = std::abs(zoom - World::ZOOM_LADDER.front());
|
||||
for (int index = 1; index < (int)World::ZOOM_LADDER.size(); index++)
|
||||
{
|
||||
for (int index = 0; index < World::ZOOM_LADDER_LEVEL_COUNT; ++index)
|
||||
if (zoom < zoom_ladder_level_get(index) - ZOOM_LADDER_EPSILON) return index;
|
||||
return World::ZOOM_LADDER_LEVEL_COUNT - 1;
|
||||
auto nextDistance = std::abs(zoom - World::ZOOM_LADDER[index]);
|
||||
if (nextDistance >= distance) continue;
|
||||
distance = nextDistance;
|
||||
result = index;
|
||||
}
|
||||
|
||||
for (int index = World::ZOOM_LADDER_LEVEL_COUNT - 1; index >= 0; --index)
|
||||
if (zoom > zoom_ladder_level_get(index) + ZOOM_LADDER_EPSILON) return index;
|
||||
return 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
float zoom_ladder_zoom_get(float zoom, float wheel)
|
||||
bool bounds_is_valid(glm::vec4 value)
|
||||
{
|
||||
auto direction = wheel > ZOOM_WHEEL_ZERO ? 1 : -1;
|
||||
auto stepCount = std::max(1, (int)std::ceil(std::abs(wheel)));
|
||||
auto index = zoom_ladder_next_index_get(zoom, direction);
|
||||
index = std::clamp(index + ((stepCount - 1) * direction), 0, World::ZOOM_LADDER_LEVEL_COUNT - 1);
|
||||
return zoom_ladder_level_get(index);
|
||||
return std::isfinite(value.x) && std::isfinite(value.y) && std::isfinite(value.z) && std::isfinite(value.w) &&
|
||||
value.z > value.x && value.w > value.y;
|
||||
}
|
||||
|
||||
void World::bounds_set(const glm::vec4& newBounds)
|
||||
{
|
||||
bounds = bounds_is_valid(newBounds) ? newBounds : BOUNDS;
|
||||
size = {bounds.z - bounds.x, bounds.w - bounds.y};
|
||||
}
|
||||
|
||||
void World::zoom_index_set(Canvas& canvas)
|
||||
{
|
||||
zoomIndex = zoom_ladder_index_get(canvas.zoom);
|
||||
canvas.zoom = ZOOM_LADDER[zoomIndex];
|
||||
}
|
||||
|
||||
void World::set(Entity& character, Canvas& canvas, Focus focus)
|
||||
@@ -82,14 +85,17 @@ namespace game::state::play
|
||||
if (cursorRoot)
|
||||
if (auto animation = cursorRoot->animationZoom.get()) cursor.play({*animation});
|
||||
|
||||
zoom = zoom_ladder_zoom_get(zoom, io.MouseWheel);
|
||||
auto direction = io.MouseWheel > ZOOM_WHEEL_ZERO ? ONE_INT : -ONE_INT;
|
||||
zoomIndex = std::clamp(zoomIndex + direction, 0, (int)ZOOM_LADDER.size() - ONE_INT);
|
||||
zoom = ZOOM_LADDER[zoomIndex];
|
||||
|
||||
auto zoomFactorAfter = math::to_unit(zoom);
|
||||
pan = cursorWorld - (cursorPos / zoomFactorAfter);
|
||||
}
|
||||
}
|
||||
|
||||
zoom = glm::clamp(ZOOM_MIN, zoom, ZOOM_MAX);
|
||||
zoomIndex = zoom_ladder_index_get(zoom);
|
||||
zoom = ZOOM_LADDER[zoomIndex];
|
||||
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Home)) character_focus(character, canvas, focus);
|
||||
}
|
||||
@@ -98,37 +104,28 @@ namespace game::state::play
|
||||
{
|
||||
static constexpr float MENU_WIDTH_MULTIPLIER = 0.30f;
|
||||
static constexpr float TOOLS_WIDTH_MULTIPLIER = 0.10f;
|
||||
static constexpr float INFO_HEIGHT_MULTIPLIER = 4.0f;
|
||||
static constexpr float PADDING = 100.0f;
|
||||
|
||||
auto rect = character.rect();
|
||||
|
||||
if (!std::isfinite(rect.x) || !std::isfinite(rect.y) || !std::isfinite(rect.z) || !std::isfinite(rect.w) ||
|
||||
rect.z <= 0.0f || rect.w <= 0.0f)
|
||||
rect.z <= ZERO_FLOAT || rect.w <= ZERO_FLOAT)
|
||||
return;
|
||||
|
||||
rect = {rect.x - PADDING * 0.5f, rect.y - PADDING * 0.5f, rect.z + PADDING, rect.w + PADDING};
|
||||
|
||||
auto infoHeightPixels =
|
||||
ImGui::GetTextLineHeightWithSpacing() * INFO_HEIGHT_MULTIPLIER + ImGui::GetStyle().WindowPadding.y * 2.0f;
|
||||
auto usableHeightPixels = std::max(1.0f, (float)canvas.size.y - infoHeightPixels);
|
||||
|
||||
auto zoomFactor = std::min((float)canvas.size.x / rect.z, usableHeightPixels / rect.w);
|
||||
auto zoomFactor = std::min((float)canvas.size.x / rect.z, (float)canvas.size.y / rect.w);
|
||||
canvas.zoom = glm::clamp(ZOOM_MIN, math::to_percent(zoomFactor), ZOOM_MAX);
|
||||
zoom_index_set(canvas);
|
||||
zoomFactor = math::to_unit(canvas.zoom);
|
||||
|
||||
auto rectCenter = glm::vec2(rect.x + rect.z * 0.5f, rect.y + rect.w * 0.5f);
|
||||
auto viewSizeWorld = glm::vec2(canvas.size) / zoomFactor;
|
||||
auto infoHeightWorld = infoHeightPixels / zoomFactor;
|
||||
canvas.pan = rectCenter - vec2(viewSizeWorld.x * 0.5f, (viewSizeWorld.y + infoHeightWorld) * 0.5f);
|
||||
canvas.pan = rectCenter - viewSizeWorld * 0.5f;
|
||||
auto menuWidthWorld = (canvas.size.x * MENU_WIDTH_MULTIPLIER) / zoomFactor;
|
||||
auto toolsWidthWorld = (canvas.size.x * TOOLS_WIDTH_MULTIPLIER) / zoomFactor;
|
||||
|
||||
if (focus == Focus::MENU || focus == Focus::MENU_TOOLS) canvas.pan.x += menuWidthWorld * 0.5f;
|
||||
if (focus == Focus::TOOLS || focus == Focus::MENU_TOOLS) canvas.pan.x -= toolsWidthWorld * 0.5f;
|
||||
|
||||
auto panMin = glm::vec2(0.0f, 0.0f);
|
||||
auto panMax = glm::max(glm::vec2(0.0f), SIZE - viewSizeWorld);
|
||||
canvas.pan = glm::clamp(panMin, canvas.pan, panMax);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "../../render/canvas.hpp"
|
||||
#include <array>
|
||||
|
||||
#include "../../entity.hpp"
|
||||
#include "../../render/canvas.hpp"
|
||||
|
||||
#include "character_manager.hpp"
|
||||
#include "item_manager.hpp"
|
||||
@@ -11,13 +13,13 @@ namespace game::state::play
|
||||
class World
|
||||
{
|
||||
public:
|
||||
static constexpr auto ZOOM_MIN = 50.0f;
|
||||
static constexpr auto ZOOM_MIN = 100.0f;
|
||||
static constexpr auto ZOOM_BASE = 100.0f;
|
||||
static constexpr auto ZOOM_LADDER_LEVEL_COUNT = 10;
|
||||
static constexpr auto ZOOM_MAX = 400.0f;
|
||||
static constexpr auto SIZE = glm::vec2{1920, 1080};
|
||||
static constexpr auto BOUNDS =
|
||||
glm::vec4(SIZE.x * 0.05, SIZE.y * 0.05, SIZE.x - (SIZE.x * 0.05f), SIZE.y - (SIZE.y * 0.05f));
|
||||
static constexpr auto BOUNDS = glm::vec4{96, 54, 1824, 1026};
|
||||
static constexpr std::array<float, 10> ZOOM_LADDER{100.0f, 115.0f, 130.0f, 150.0f, 175.0f,
|
||||
200.0f, 235.0f, 275.0f, 325.0f, 400.0f};
|
||||
|
||||
enum Focus
|
||||
{
|
||||
@@ -27,6 +29,12 @@ namespace game::state::play
|
||||
TOOLS
|
||||
};
|
||||
|
||||
glm::vec2 size{SIZE};
|
||||
glm::vec4 bounds{BOUNDS};
|
||||
int zoomIndex{};
|
||||
|
||||
void bounds_set(const glm::vec4& newBounds);
|
||||
void zoom_index_set(Canvas& canvas);
|
||||
void update(Entity& character, Entity& cursor, Canvas& canvas, Focus = CENTER);
|
||||
void character_focus(Entity& character, Canvas& canvas, Focus = CENTER);
|
||||
void set(Entity& character, Canvas& canvas, Focus = CENTER);
|
||||
|
||||
@@ -62,7 +62,7 @@ namespace game::state::select
|
||||
{
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(game::util::color::GRAY)));
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
ImGui::TextWrapped("%s", character->description.c_str());
|
||||
ImGui::PopFont();
|
||||
@@ -86,7 +86,7 @@ namespace game::state::select
|
||||
if (ImGui::BeginTabItem("Credits"))
|
||||
{
|
||||
ImGui::Separator();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY)));
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(game::util::color::GRAY)));
|
||||
ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2);
|
||||
if (!character->credits.empty())
|
||||
ImGui::TextWrapped("%s", character->credits.c_str());
|
||||
|
||||
@@ -13,4 +13,4 @@ namespace game::state::select
|
||||
|
||||
void update(Resources&, int characterIndex);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -68,13 +68,14 @@ namespace game::util::imgui::widget
|
||||
{
|
||||
if (!is_rect_valid(rect)) rect = entity.rect();
|
||||
|
||||
auto rectSize = glm::vec2(rect.z, rect.w);
|
||||
scale = (size.x <= 0.0f || size.y <= 0.0f || !is_rect_valid(rect))
|
||||
? 0.0f
|
||||
: std::min(size.x / rectSize.x, size.y / rectSize.y);
|
||||
auto isRectValid = is_rect_valid(rect);
|
||||
auto rectSize = isRectValid ? glm::vec2(rect.z, rect.w) : glm::vec2(1.0f);
|
||||
scale = (size.x <= 0.0f || size.y <= 0.0f || !isRectValid) ? 0.0f
|
||||
: std::min(size.x / rectSize.x, size.y / rectSize.y);
|
||||
if (maxScale > 0.0f) scale = std::min(maxScale, scale);
|
||||
|
||||
auto previewSize = rectSize * scale;
|
||||
if (!std::isfinite(previewSize.x) || !std::isfinite(previewSize.y)) previewSize = {};
|
||||
imageSize = ImVec2(previewSize.x, previewSize.y);
|
||||
auto canvasSize = glm::ivec2(std::max(1.0f, previewSize.x), std::max(1.0f, previewSize.y));
|
||||
|
||||
|
||||
+88
-32
@@ -6,6 +6,7 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
@@ -36,7 +37,9 @@ namespace game::util::physfs
|
||||
constexpr auto CRC_INITIAL = 0xffffffffu;
|
||||
constexpr auto CRC_POLYNOMIAL = 0xedb88320u;
|
||||
constexpr auto CRC_XOR = 0xffffffffu;
|
||||
constexpr auto MOUNT_EXPORT = "__developer_export__";
|
||||
constexpr auto ZIP_TEMP_EXTENSION = ".tmp";
|
||||
constexpr auto ZIP_SIZE_LIMIT = std::numeric_limits<uint32_t>::max();
|
||||
constexpr auto ZIP_ENTRY_LIMIT = std::numeric_limits<uint16_t>::max();
|
||||
|
||||
struct ZipEntry
|
||||
{
|
||||
@@ -44,6 +47,7 @@ namespace game::util::physfs
|
||||
std::vector<uint8_t> data{};
|
||||
uint32_t crc{};
|
||||
uint32_t offset{};
|
||||
uint32_t uncompressedSize{};
|
||||
};
|
||||
|
||||
uint32_t crc32_get(const std::vector<uint8_t>& data)
|
||||
@@ -83,7 +87,7 @@ namespace game::util::physfs
|
||||
uint16_write(stream, ZIP_DATE_NONE);
|
||||
uint32_write(stream, entry.crc);
|
||||
uint32_write(stream, (uint32_t)entry.data.size());
|
||||
uint32_write(stream, (uint32_t)entry.data.size());
|
||||
uint32_write(stream, entry.uncompressedSize);
|
||||
uint16_write(stream, (uint16_t)entry.path.size());
|
||||
uint16_write(stream, ZIP_EXTRA_LENGTH_NONE);
|
||||
stream.write(entry.path.data(), (std::streamsize)entry.path.size());
|
||||
@@ -100,7 +104,7 @@ namespace game::util::physfs
|
||||
uint16_write(stream, ZIP_DATE_NONE);
|
||||
uint32_write(stream, entry.crc);
|
||||
uint32_write(stream, (uint32_t)entry.data.size());
|
||||
uint32_write(stream, (uint32_t)entry.data.size());
|
||||
uint32_write(stream, entry.uncompressedSize);
|
||||
uint16_write(stream, (uint16_t)entry.path.size());
|
||||
uint16_write(stream, ZIP_EXTRA_LENGTH_NONE);
|
||||
uint16_write(stream, ZIP_COMMENT_LENGTH_NONE);
|
||||
@@ -254,46 +258,98 @@ namespace game::util::physfs
|
||||
|
||||
auto output = destination;
|
||||
if (output.extension().empty()) output += ".zip";
|
||||
if (!output.parent_path().empty()) std::filesystem::create_directories(output.parent_path(), error);
|
||||
|
||||
Archive archive(source, MOUNT_EXPORT);
|
||||
if (!archive.is_valid()) return false;
|
||||
|
||||
std::vector<std::filesystem::path> paths{};
|
||||
for (auto& entry : std::filesystem::recursive_directory_iterator(source, error))
|
||||
if (!error && entry.is_regular_file(error)) paths.emplace_back(entry.path());
|
||||
std::ranges::sort(paths);
|
||||
auto outputTemp = output;
|
||||
outputTemp += ZIP_TEMP_EXTENSION;
|
||||
|
||||
std::vector<ZipEntry> entries{};
|
||||
entries.reserve(paths.size());
|
||||
for (auto& path : paths)
|
||||
auto outputAbsolute = std::filesystem::absolute(output, error).lexically_normal();
|
||||
if (error) return false;
|
||||
auto outputTempAbsolute = std::filesystem::absolute(outputTemp, error).lexically_normal();
|
||||
if (error) return false;
|
||||
auto iterator = std::filesystem::recursive_directory_iterator(source, error);
|
||||
if (error) return false;
|
||||
|
||||
for (auto& file : iterator)
|
||||
{
|
||||
auto relative = std::filesystem::relative(path, source, error).generic_string();
|
||||
if (relative.empty() || error) continue;
|
||||
auto physfsPath = Path(std::string(archive) + "/" + relative);
|
||||
auto data = physfsPath.read();
|
||||
auto size = std::filesystem::file_size(path, error);
|
||||
if (!error && size > 0 && data.empty()) return false;
|
||||
entries.push_back({.path = relative, .data = std::move(data)});
|
||||
entries.back().crc = crc32_get(entries.back().data);
|
||||
if (error) return false;
|
||||
if (!file.is_regular_file(error)) continue;
|
||||
|
||||
auto fileAbsolute = std::filesystem::absolute(file.path(), error).lexically_normal();
|
||||
if (fileAbsolute == outputAbsolute || fileAbsolute == outputTempAbsolute) continue;
|
||||
|
||||
auto relative = std::filesystem::relative(file.path(), source, error);
|
||||
if (error) return false;
|
||||
auto relativeString = relative.generic_string();
|
||||
if (relativeString.size() > ZIP_ENTRY_LIMIT) return false;
|
||||
|
||||
auto size = std::filesystem::file_size(file.path(), error);
|
||||
if (error || size > ZIP_SIZE_LIMIT) return false;
|
||||
|
||||
std::vector<uint8_t> data((size_t)size);
|
||||
std::ifstream input(file.path(), std::ios::binary);
|
||||
if (!input) return false;
|
||||
if (!data.empty()) input.read((char*)data.data(), (std::streamsize)data.size());
|
||||
if (!input) return false;
|
||||
|
||||
entries.emplace_back(std::move(relativeString), std::move(data), crc32_get(data), ZIP_ATTRIBUTE_NONE,
|
||||
(uint32_t)data.size());
|
||||
}
|
||||
|
||||
std::ofstream stream(output, std::ios::binary | std::ios::trunc);
|
||||
if (!stream.is_open()) return false;
|
||||
if (entries.size() > ZIP_ENTRY_LIMIT) return false;
|
||||
std::ranges::sort(entries, {}, &ZipEntry::path);
|
||||
|
||||
auto parent = output.parent_path();
|
||||
if (!parent.empty())
|
||||
{
|
||||
std::filesystem::create_directories(parent, error);
|
||||
if (error) return false;
|
||||
}
|
||||
|
||||
std::filesystem::remove(outputTemp, error);
|
||||
error.clear();
|
||||
|
||||
std::ofstream stream(outputTemp, std::ios::binary | std::ios::trunc);
|
||||
if (!stream) return false;
|
||||
|
||||
for (auto& entry : entries)
|
||||
{
|
||||
entry.offset = (uint32_t)stream.tellp();
|
||||
auto offset = stream.tellp();
|
||||
if (offset < 0 || (uint64_t)offset > ZIP_SIZE_LIMIT) return false;
|
||||
entry.offset = (uint32_t)offset;
|
||||
local_header_write(stream, entry);
|
||||
if (!entry.data.empty()) stream.write((const char*)entry.data.data(), (std::streamsize)entry.data.size());
|
||||
if (!entry.data.empty()) stream.write((char*)entry.data.data(), (std::streamsize)entry.data.size());
|
||||
}
|
||||
|
||||
auto directoryOffset = (uint32_t)stream.tellp();
|
||||
for (auto& entry : entries)
|
||||
central_header_write(stream, entry);
|
||||
auto directorySize = (uint32_t)stream.tellp() - directoryOffset;
|
||||
end_header_write(stream, (uint16_t)entries.size(), directorySize, directoryOffset);
|
||||
auto directoryOffset = stream.tellp();
|
||||
if (directoryOffset < 0 || (uint64_t)directoryOffset > ZIP_SIZE_LIMIT) return false;
|
||||
|
||||
return stream.good();
|
||||
for (const auto& entry : entries)
|
||||
central_header_write(stream, entry);
|
||||
|
||||
auto directoryEnd = stream.tellp();
|
||||
if (directoryEnd < 0 || (uint64_t)directoryEnd > ZIP_SIZE_LIMIT) return false;
|
||||
auto directorySize = (uint64_t)directoryEnd - (uint64_t)directoryOffset;
|
||||
if (directorySize > ZIP_SIZE_LIMIT) return false;
|
||||
|
||||
end_header_write(stream, (uint16_t)entries.size(), (uint32_t)directorySize, (uint32_t)directoryOffset);
|
||||
stream.close();
|
||||
|
||||
if (!stream)
|
||||
{
|
||||
std::filesystem::remove(outputTemp, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::rename(outputTemp, output, error);
|
||||
if (error)
|
||||
{
|
||||
error.clear();
|
||||
std::filesystem::remove(output, error);
|
||||
if (error) return false;
|
||||
std::filesystem::rename(outputTemp, output, error);
|
||||
}
|
||||
|
||||
if (error) return false;
|
||||
return std::filesystem::is_regular_file(output, error);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user