From af04a9b31337c26781e9ca96d2a33ccca56cf84e Mon Sep 17 00:00:00 2001 From: shweet Date: Wed, 1 Apr 2026 02:08:50 -0400 Subject: [PATCH] a (not so) mini update --- .gitignore | 1 + .vscode/launch.json | 10 +- .vscode/tasks.json | 12 +- CMakeLists.txt | 39 +++-- Icon.ico | Bin 9662 -> 7114 bytes MODDING.md | 23 ++- README.md | 4 +- cmake/copy_resources.cmake | 58 +++---- cmake/create_index_zip.cmake | 6 +- snivy.rc => shweets-sim.rc | 0 src/entity/actor.cpp | 22 ++- src/entity/character.cpp | 70 ++++---- src/entity/character.hpp | 2 +- src/entity/item.cpp | 4 +- src/entity/item.hpp | 4 +- src/loader.cpp | 6 +- src/resource/font.hpp | 6 +- src/resource/xml/character.cpp | 43 +++-- src/resource/xml/character.hpp | 43 ++--- src/resource/xml/character_preview.cpp | 2 +- src/resource/xml/character_preview.hpp | 2 +- src/resource/xml/dialogue.cpp | 32 +++- src/resource/xml/dialogue.hpp | 12 +- src/resource/xml/item.cpp | 8 +- src/resource/xml/item.hpp | 8 +- src/resource/xml/save.cpp | 5 +- src/resource/xml/save.hpp | 2 +- src/resource/xml/settings.hpp | 2 +- src/resource/xml/strings.cpp | 48 ++++++ src/resource/xml/strings.hpp | 159 ++++++++++++++++++ src/resource/xml/util.cpp | 23 +++ src/resource/xml/util.hpp | 2 + src/state/play.cpp | 62 ++++--- src/state/play/arcade.cpp | 76 +++++++++ src/state/play/arcade.hpp | 26 +++ src/state/play/arcade/skill_check.cpp | 47 ++++-- src/state/play/arcade/skill_check.hpp | 2 +- src/state/play/character_manager.cpp | 10 +- src/state/play/chat.cpp | 32 ---- src/state/play/cheats.cpp | 88 ++++------ src/state/play/cheats.hpp | 3 +- src/state/play/debug.cpp | 59 +++++-- src/state/play/debug.hpp | 3 +- src/state/play/info.cpp | 33 ++-- src/state/play/interact.cpp | 57 +++++++ src/state/play/{chat.hpp => interact.hpp} | 2 +- src/state/play/inventory.cpp | 196 ++++++++++++++++------ src/state/play/item_manager.cpp | 67 +++++--- src/state/play/menu.cpp | 45 +++-- src/state/play/menu.hpp | 15 +- src/state/play/stats.cpp | 48 ------ src/state/play/stats.hpp | 17 -- src/state/play/style.hpp | 14 ++ src/state/play/text.cpp | 8 +- src/state/play/tools.cpp | 11 +- src/state/play/world.cpp | 10 +- src/state/select/info.cpp | 67 +++++--- src/state/select/preview.cpp | 2 +- src/state/settings_menu.cpp | 60 ++++--- src/state/settings_menu.hpp | 3 +- src/util/imgui/style.cpp | 8 +- src/util/preferences.cpp | 4 +- src/util/web_filesystem.cpp | 6 +- web/index.html | 2 +- 64 files changed, 1158 insertions(+), 583 deletions(-) rename snivy.rc => shweets-sim.rc (100%) create mode 100644 src/resource/xml/strings.cpp create mode 100644 src/resource/xml/strings.hpp create mode 100644 src/state/play/arcade.cpp create mode 100644 src/state/play/arcade.hpp delete mode 100644 src/state/play/chat.cpp create mode 100644 src/state/play/interact.cpp rename src/state/play/{chat.hpp => interact.hpp} (90%) delete mode 100644 src/state/play/stats.cpp delete mode 100644 src/state/play/stats.hpp create mode 100644 src/state/play/style.hpp diff --git a/.gitignore b/.gitignore index b327771..8c66a73 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ build/ build-web/ +concept/ resources/ release/ out/ diff --git a/.vscode/launch.json b/.vscode/launch.json index acc7572..7ec7abb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "cppdbg", "request": "launch", "preLaunchTask": "cmake: build debug", - "program": "${workspaceFolder}/out/build/linux-debug/bin/Debug/snivy", + "program": "${workspaceFolder}/out/build/linux-debug/bin/Debug/shweets-sim", "args": [], "stopAtEntry": false, "cwd": "${workspaceFolder}/out/build/linux-debug/bin/Debug", @@ -26,7 +26,7 @@ ], "windows": { "type": "cppvsdbg", - "program": "${workspaceFolder}/out/build/x64-Debug/bin/Debug/snivy.exe", + "program": "${workspaceFolder}/out/build/x64-Debug/bin/Debug/shweets-sim.exe", "cwd": "${workspaceFolder}/out/build/x64-Debug/bin/Debug" } }, @@ -36,7 +36,7 @@ "request": "launch", "noDebug": true, "preLaunchTask": "cmake: build release", - "program": "${workspaceFolder}/out/build/linux-release/bin/Release/snivy", + "program": "${workspaceFolder}/out/build/linux-release/bin/Release/shweets-sim", "args": [], "cwd": "${workspaceFolder}/out/build/linux-release/bin/Release", "environment": [], @@ -45,7 +45,7 @@ "miDebuggerPath": "/usr/bin/gdb", "windows": { "type": "cppvsdbg", - "program": "${workspaceFolder}/out/build/x64-Release/bin/Release/snivy.exe", + "program": "${workspaceFolder}/out/build/x64-Release/bin/Release/shweets-sim.exe", "cwd": "${workspaceFolder}/out/build/x64-Release/bin/Release" } }, @@ -58,4 +58,4 @@ "cwd": "${workspaceFolder}" } ] -} +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5f8507e..aac2efa 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -59,7 +59,7 @@ "label": "linux: run debug", "type": "shell", "linux": { - "command": "${workspaceFolder}/out/build/linux-debug/bin/Debug/snivy" + "command": "${workspaceFolder}/out/build/linux-debug/bin/Debug/shweets-sim" }, "options": { "cwd": "${workspaceFolder}/out/build/linux-debug/bin/Debug" @@ -74,7 +74,7 @@ "label": "linux: run release", "type": "shell", "linux": { - "command": "${workspaceFolder}/out/build/linux-release/bin/Release/snivy" + "command": "${workspaceFolder}/out/build/linux-release/bin/Release/shweets-sim" }, "options": { "cwd": "${workspaceFolder}/out/build/linux-release/bin/Release" @@ -112,7 +112,7 @@ "label": "web: serve", "type": "shell", "linux": { - "command": "sh -lc 'PID_FILE=/tmp/snivy-web-http.pid; LOG_FILE=/tmp/snivy-web-http.log; WEB_ROOT=out/build/web; URL=http://127.0.0.1:8000/bin/Release/index.html; [ -f \"$WEB_ROOT/bin/Release/index.html\" ] || { echo \"Web output not found at $WEB_ROOT/bin/Release/index.html (run web: build)\"; exit 1; }; if [ -f \"$PID_FILE\" ] && kill -0 \"$(cat \"$PID_FILE\")\" 2>/dev/null; then kill \"$(cat \"$PID_FILE\")\" >/dev/null 2>&1 || true; rm -f \"$PID_FILE\"; fi; nohup python3 -m http.server 8000 --bind 127.0.0.1 --directory \"$WEB_ROOT\" >\"$LOG_FILE\" 2>&1 & echo $! >\"$PID_FILE\"; echo \"Started web server on http://127.0.0.1:8000 (root: $WEB_ROOT)\"; READY=0; for _ in $(seq 1 40); do if command -v curl >/dev/null 2>&1; then curl -fsS \"$URL\" >/dev/null 2>&1 && READY=1 && break; elif command -v wget >/dev/null 2>&1; then wget -qO- \"$URL\" >/dev/null 2>&1 && READY=1 && break; fi; sleep 0.1; done; [ \"$READY\" = \"1\" ] || { echo \"Web server did not become ready. See /tmp/snivy-web-http.log\"; exit 1; }'" + "command": "sh -lc 'PID_FILE=/tmp/shweets-sim-web-http.pid; LOG_FILE=/tmp/shweets-sim-web-http.log; WEB_ROOT=out/build/web; URL=http://127.0.0.1:8000/bin/Release/index.html; [ -f \"$WEB_ROOT/bin/Release/index.html\" ] || { echo \"Web output not found at $WEB_ROOT/bin/Release/index.html (run web: build)\"; exit 1; }; if [ -f \"$PID_FILE\" ] && kill -0 \"$(cat \"$PID_FILE\")\" 2>/dev/null; then kill \"$(cat \"$PID_FILE\")\" >/dev/null 2>&1 || true; rm -f \"$PID_FILE\"; fi; nohup python3 -m http.server 8000 --bind 127.0.0.1 --directory \"$WEB_ROOT\" >\"$LOG_FILE\" 2>&1 & echo $! >\"$PID_FILE\"; echo \"Started web server on http://127.0.0.1:8000 (root: $WEB_ROOT)\"; READY=0; for _ in $(seq 1 40); do if command -v curl >/dev/null 2>&1; then curl -fsS \"$URL\" >/dev/null 2>&1 && READY=1 && break; elif command -v wget >/dev/null 2>&1; then wget -qO- \"$URL\" >/dev/null 2>&1 && READY=1 && break; fi; sleep 0.1; done; [ \"$READY\" = \"1\" ] || { echo \"Web server did not become ready. See /tmp/shweets-sim-web-http.log\"; exit 1; }'" }, "windows": { "command": "echo \"web tasks are configured for Linux (emsdk + python + chromium).\" && exit 1" @@ -123,7 +123,7 @@ "label": "web: stop server", "type": "shell", "linux": { - "command": "sh -lc 'PID_FILE=/tmp/snivy-web-http.pid; if [ -f \"$PID_FILE\" ] && kill -0 \"$(cat \"$PID_FILE\")\" 2>/dev/null; then kill \"$(cat \"$PID_FILE\")\" && rm -f \"$PID_FILE\" && echo \"Stopped web server\"; else rm -f \"$PID_FILE\" && echo \"Web server not running\"; fi'" + "command": "sh -lc 'PID_FILE=/tmp/shweets-sim-web-http.pid; if [ -f \"$PID_FILE\" ] && kill -0 \"$(cat \"$PID_FILE\")\" 2>/dev/null; then kill \"$(cat \"$PID_FILE\")\" && rm -f \"$PID_FILE\" && echo \"Stopped web server\"; else rm -f \"$PID_FILE\" && echo \"Web server not running\"; fi'" }, "windows": { "command": "echo \"web tasks are configured for Linux (emsdk + python + chromium).\" && exit 1" @@ -154,7 +154,7 @@ "label": "web: run", "type": "shell", "linux": { - "command": "sh -lc 'set -e; if ! command -v emcmake >/dev/null 2>&1; then if [ -n \"$EMSDK\" ] && [ -f \"$EMSDK/emsdk_env.sh\" ]; then . \"$EMSDK/emsdk_env.sh\" >/dev/null 2>&1; elif [ -f \"$HOME/emsdk/emsdk_env.sh\" ]; then . \"$HOME/emsdk/emsdk_env.sh\" >/dev/null 2>&1; elif [ -f \"../emsdk/emsdk_env.sh\" ]; then . \"../emsdk/emsdk_env.sh\" >/dev/null 2>&1; else echo \"Emscripten not found. Install emsdk or set EMSDK env var.\"; exit 1; fi; fi; emcmake cmake -S . -B out/build/web -DCMAKE_BUILD_TYPE=Release; cmake --build out/build/web -j$(nproc); PID_FILE=/tmp/snivy-web-http.pid; LOG_FILE=/tmp/snivy-web-http.log; WEB_ROOT=out/build/web; URL=http://127.0.0.1:8000/bin/Release/index.html; [ -f \"$WEB_ROOT/bin/Release/index.html\" ] || { echo \"Web output not found at $WEB_ROOT/bin/Release/index.html\"; exit 1; }; if [ -f \"$PID_FILE\" ] && kill -0 \"$(cat \"$PID_FILE\")\" 2>/dev/null; then kill \"$(cat \"$PID_FILE\")\" >/dev/null 2>&1 || true; rm -f \"$PID_FILE\"; fi; nohup python3 -m http.server 8000 --bind 127.0.0.1 --directory \"$WEB_ROOT\" >\"$LOG_FILE\" 2>&1 & echo $! >\"$PID_FILE\"; echo \"Started web server on http://127.0.0.1:8000 (root: $WEB_ROOT)\"; READY=0; for _ in $(seq 1 40); do if command -v curl >/dev/null 2>&1; then curl -fsS \"$URL\" >/dev/null 2>&1 && READY=1 && break; elif command -v wget >/dev/null 2>&1; then wget -qO- \"$URL\" >/dev/null 2>&1 && READY=1 && break; fi; sleep 0.1; done; [ \"$READY\" = \"1\" ] || { echo \"Web server did not become ready. See /tmp/snivy-web-http.log\"; exit 1; }; (xdg-open \"$URL\" >/dev/null 2>&1 || chromium --new-tab \"$URL\" >/dev/null 2>&1 || chromium-browser --new-tab \"$URL\" >/dev/null 2>&1 || google-chrome --new-tab \"$URL\" >/dev/null 2>&1 || google-chrome-stable --new-tab \"$URL\" >/dev/null 2>&1 || python3 -m webbrowser \"$URL\" >/dev/null 2>&1 || { echo \"Could not launch browser automatically. Open: $URL\"; exit 0; }) & sleep 0.15; echo \"Requested browser open: $URL\"'" + "command": "sh -lc 'set -e; if ! command -v emcmake >/dev/null 2>&1; then if [ -n \"$EMSDK\" ] && [ -f \"$EMSDK/emsdk_env.sh\" ]; then . \"$EMSDK/emsdk_env.sh\" >/dev/null 2>&1; elif [ -f \"$HOME/emsdk/emsdk_env.sh\" ]; then . \"$HOME/emsdk/emsdk_env.sh\" >/dev/null 2>&1; elif [ -f \"../emsdk/emsdk_env.sh\" ]; then . \"../emsdk/emsdk_env.sh\" >/dev/null 2>&1; else echo \"Emscripten not found. Install emsdk or set EMSDK env var.\"; exit 1; fi; fi; emcmake cmake -S . -B out/build/web -DCMAKE_BUILD_TYPE=Release; cmake --build out/build/web -j$(nproc); PID_FILE=/tmp/shweets-sim-web-http.pid; LOG_FILE=/tmp/shweets-sim-web-http.log; WEB_ROOT=out/build/web; URL=http://127.0.0.1:8000/bin/Release/index.html; [ -f \"$WEB_ROOT/bin/Release/index.html\" ] || { echo \"Web output not found at $WEB_ROOT/bin/Release/index.html\"; exit 1; }; if [ -f \"$PID_FILE\" ] && kill -0 \"$(cat \"$PID_FILE\")\" 2>/dev/null; then kill \"$(cat \"$PID_FILE\")\" >/dev/null 2>&1 || true; rm -f \"$PID_FILE\"; fi; nohup python3 -m http.server 8000 --bind 127.0.0.1 --directory \"$WEB_ROOT\" >\"$LOG_FILE\" 2>&1 & echo $! >\"$PID_FILE\"; echo \"Started web server on http://127.0.0.1:8000 (root: $WEB_ROOT)\"; READY=0; for _ in $(seq 1 40); do if command -v curl >/dev/null 2>&1; then curl -fsS \"$URL\" >/dev/null 2>&1 && READY=1 && break; elif command -v wget >/dev/null 2>&1; then wget -qO- \"$URL\" >/dev/null 2>&1 && READY=1 && break; fi; sleep 0.1; done; [ \"$READY\" = \"1\" ] || { echo \"Web server did not become ready. See /tmp/shweets-sim-web-http.log\"; exit 1; }; (xdg-open \"$URL\" >/dev/null 2>&1 || chromium --new-tab \"$URL\" >/dev/null 2>&1 || chromium-browser --new-tab \"$URL\" >/dev/null 2>&1 || google-chrome --new-tab \"$URL\" >/dev/null 2>&1 || google-chrome-stable --new-tab \"$URL\" >/dev/null 2>&1 || python3 -m webbrowser \"$URL\" >/dev/null 2>&1 || { echo \"Could not launch browser automatically. Open: $URL\"; exit 0; }) & sleep 0.15; echo \"Requested browser open: $URL\"'" }, "windows": { "command": "echo \"web tasks are configured for Linux (emsdk + python + chromium).\" && exit 1" @@ -162,4 +162,4 @@ "problemMatcher": [] } ] -} +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index beedec8..4738e9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.27) -project(snivy LANGUAGES C CXX) +project(shweets-sim LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -132,7 +132,7 @@ add_executable(${PROJECT_NAME} if(WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") enable_language(RC) target_sources(${PROJECT_NAME} PRIVATE - "${CMAKE_CURRENT_SOURCE_DIR}/snivy.rc" + "${CMAKE_CURRENT_SOURCE_DIR}/shweets-sim.rc" "${CMAKE_CURRENT_SOURCE_DIR}/Icon.ico" ) endif() @@ -205,16 +205,23 @@ set(PROJECT_RESOURCES_BINARY_DIR "${CMAKE_BINARY_DIR}/bin/$/resources") if(EXISTS "${PROJECT_RESOURCES_DIR}") file(GLOB_RECURSE PROJECT_RESOURCE_FILES CONFIGURE_DEPENDS "${PROJECT_RESOURCES_DIR}/*") - if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") - add_custom_target(copy_resources ALL - COMMAND ${CMAKE_COMMAND} - -DSRC_DIR="${PROJECT_RESOURCES_DIR}" - -DDST_DIR="${PROJECT_RESOURCES_BINARY_DIR}" - -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/copy_resources.cmake" - DEPENDS ${PROJECT_RESOURCE_FILES} - COMMENT "Copying resources directory") - add_dependencies(${PROJECT_NAME} copy_resources) - endif() + list(FILTER PROJECT_RESOURCE_FILES EXCLUDE REGEX ".*/characters/[^/]+\\.zip$") + set(COPY_RESOURCES_STAMP "${CMAKE_BINARY_DIR}/CMakeFiles/copy_resources.stamp") + add_custom_command( + OUTPUT "${COPY_RESOURCES_STAMP}" + COMMAND ${CMAKE_COMMAND} + -DSRC_DIR=${PROJECT_RESOURCES_DIR} + -DDST_DIR=${PROJECT_RESOURCES_BINARY_DIR} + -DSTAMP_FILE=${COPY_RESOURCES_STAMP} + -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/copy_resources.cmake" + DEPENDS ${PROJECT_RESOURCE_FILES} + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/copy_resources.cmake" + COMMENT "Copying resources directory" + VERBATIM + ) + add_custom_target(copy_resources ALL + DEPENDS "${COPY_RESOURCES_STAMP}") + add_dependencies(${PROJECT_NAME} copy_resources) set(HAS_PROJECT_RESOURCES TRUE) else() set(HAS_PROJECT_RESOURCES FALSE) @@ -243,7 +250,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") if(HAS_PROJECT_RESOURCES) target_link_options(${PROJECT_NAME} PRIVATE "--preload-file" - "${PROJECT_RESOURCES_DIR}@resources" + "${PROJECT_RESOURCES_BINARY_DIR}@resources" ) endif() set_target_properties(${PROJECT_NAME} PROPERTIES @@ -254,7 +261,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten") COMMAND ${CMAKE_COMMAND} -DBIN_DIR="$" -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/create_index_zip.cmake" - COMMENT "Creating snivy-web.zip from Emscripten output") + COMMENT "Creating shweets-sim-web.zip from Emscripten output") else() find_package(OpenGL REQUIRED COMPONENTS OpenGL) target_link_libraries(${PROJECT_NAME} PRIVATE OpenGL::GL) @@ -266,9 +273,9 @@ if(WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") -DBIN_ROOT="${CMAKE_BINARY_DIR}/bin" -DTARGET_DIR="$" -DEXE_FILE="$" - -DPACKAGE_NAME="snivy-win32" + -DPACKAGE_NAME="shweets-sim-win32" -P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/create_windows_zip.cmake" - COMMENT "Creating snivy-win32 package") + COMMENT "Creating shweets-sim-win32 package") endif() message(STATUS "System: ${CMAKE_SYSTEM_NAME}") diff --git a/Icon.ico b/Icon.ico index 269a2bb8971d830a986eb807285a41838b37461e..d16a6c73f01789b3889fd75a9834d39c53bf56e0 100644 GIT binary patch literal 7114 zcmd6sJ(3eK6oln+;LsTffyow%L--b)VQ*m{fUA&k0B0P5)Cm~@p{d%kc&+irzL}aI zNi$Qm+LBv(uU~hMf1(XxV}DPdgS~zT-`|JuE`;#W_LJ=|wpG{qYP}`a^S7U|W-sC0 zr%{t-Y^Kknrq=cN@%tWYc2}n+i=RQSCd=3iy51}2{OQ*n zQSb7XzUTPBQ9jN~=cv^q&s)r|MC}{A674x${HAg#rRVCr*782>cW3y<44Jrt z)Ju{1J{(T-+F@?Y7Y;bYr4LT=UQ_g+rgVn%=hX4+Pt>l$o#R(Bi(flM{|#=5ek~WT z{v`cXW@CB&{8hWU`|%J z9;aRN7-;6?7~`Om=bFz#cg%aLhep0@J`-(W>2kCAhJ{S$k-mF58JQ`xs;1-T(dYDa zovV#z43fd;;!JOb{H#&@$pccKB|c*qfAR>a31>(zGU~JV^CIF4h^(0@-ohcCLWq~J zO?VEQgqB+$%wM7P{}>vCtL~qds%AEi`##}0>{I<>?~D4^ ba7=g$r-b(q8{9Ns=a=o{zKdx-9uocowW4*0 literal 9662 zcmeI2J#G|15QWQf;LsVVogfifTwt$4!WEqECWw$TIO77uY(@qru7JeyE6rHV>-u}|RSO{ki=64t&5h`v*YfqP$Qu!Pr$a@@y|`BwhnLzjOmK=Lv4J-Hpw086~n%WR0= z7k)fi-N&iB^@ZHTUc>HqD#4J07(W(s_n6B29=pRALwc!|_@zDgPyTG(#`7F?`_;rJknDqgCrQH3zTvFx7voFO{==%ER2eafiCGn7$C% zJvTGgl0N2e`yKqd`@cJL z_>VEm4lz82ehZCL_U`v*51%`=J3D}<=HL9{-S8Csub$Tz_#ah3j@W%o5X^qpq*Z3DE C+N>A= diff --git a/MODDING.md b/MODDING.md index 7128380..9a8485a 100644 --- a/MODDING.md +++ b/MODDING.md @@ -1,10 +1,10 @@ -# Modding Feed Snivy +# Modding Shweet's Sim -Want to add characters or modify existing ones or mod Feed Snivy in general or otherwise want to know how the game works? Here's how. +Want to add characters or modify existing ones or mod Shweet's Sim in general or otherwise want to know how the game works? Here's how. # Animation Format -Feed Snivy uses a semi-proprietary format called ".anm2" for tweened character animations, sourced from a spritesheet. This file format comes from the game _The Binding of Isaac: Rebirth_ (a game I mod on my own time) for animations. +Shweet's Sim uses a semi-proprietary format called ".anm2" for tweened character animations, sourced from a spritesheet. This file format comes from the game _The Binding of Isaac: Rebirth_ (a game I mod on my own time) for animations. You can either use my own animation editor for that game and this one (recommended), [Anm2Ed](https://github.com/ShweetsStuff/anm2ed), or if you have that game on Steam, you can find that game's own proprietary animation editor in that game's Steam folder and then in tools/IsaacAnimationEditor. I can't guarantee the stability or perfect efficacy of either, but Anm2Ed has crash mitigations and autosave (but of course, save often, for whatever you do). @@ -26,7 +26,7 @@ There's two folders inside resources; "characters" and "font". EVERYTHING ELSE is stored inside bespoke character archives (.zips) in the "characters" folder. This has all data associated with characters. Think of characters more as tailored game experiences rather than literally being just the characters. Not only is there the character graphics, but backgrounds, items, parameters, etc. These are intensely customizable to suit whatever experience you'd like (within the confines of the engine, of course). -Feed Snivy uses a collection of [XML](https://en.wikipedia.org/wiki/XML) files to parse data; make sure to brush up on the format. The engine can expect six files in the archive's root: +Shweet's Sim uses a collection of [XML](https://en.wikipedia.org/wiki/XML) files to parse data; make sure to brush up on the format. The engine can expect six files in the archive's root: - areas.xml - character.xml @@ -34,7 +34,7 @@ Feed Snivy uses a collection of [XML](https://en.wikipedia.org/wiki/XML) files t - dialogue.xml - items.xml - menu.xml -- play.xml +- skill_check.xml (dialogue.xml may or may not be optional; but in future updates I'll make sure of it, for dialogueless characters). @@ -460,10 +460,10 @@ Sound that will play after entering a special code to activate cheats. #### Sound (path) The sound that will play, based on SoundRootPath. -## play.xml -Determines behavior and appearance of the "Play" minigame. +## skill_check.xml +Determines behavior and appearance of the "Skill Check" minigame. -### Play +### SkillCheck #### SoundRootPath (path) Working folder/directory of where used sounds will be contained within. #### RewardScore (int) @@ -529,7 +529,7 @@ This sound will play when the grade is hit. # Saves -Outside of resources, Feed Snivy also has a few files it writes outside of the game. You will find these in %AppData%/snivy on Windows and ~/.local/share/snivy on Linux. +Outside of resources, Shweet's Sim also has a few files it writes outside of the game. You will find these in %AppData%/shweets-sim on Windows and ~/.local/share/shweets-sim on Linux. ## settings.xml Stores general game settings and configuration; beyond invididual characters. @@ -574,8 +574,7 @@ When digestion bar is going down, this is the remaining time to 0 (in ticks) Total calories consumed by the character, per save file. #### TotalFoodItemsEaten (int) How many food items have been completely consumed by the character, per save file. - -### Play +### SkillCheck #### TotalPlays (int) However many times the "play" game has been attempted (hitting the bar counts as one "play") #### HighScore (int) @@ -599,4 +598,4 @@ Count of the item. # Conclusion -Hopefully this'll give you the resources you need to start making your own characters. If you need any help with this guide, or with clarification on anything, I'm available. Additionally, the game is [licensed as free software](https://github.com/ShweetsStuff/snivy), meaning if you're stuck the code should give you a clue (though I apologize for the lack of comments. Self-documenting code though, am I right? :^) ) +Hopefully this'll give you the resources you need to start making your own characters. If you need any help with this guide, or with clarification on anything, I'm available. Additionally, the game is [licensed as free software](https://github.com/ShweetsStuff/shweets-sim), meaning if you're stuck the code should give you a clue (though I apologize for the lack of comments. Self-documenting code though, am I right? :^) ) diff --git a/README.md b/README.md index 460d573..8b35f9c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Feed Snivy +# Shweet's Sim ![Preview](screenshots/preview.png) -This Is A Video Game Where You Feed The Snivy. +Interactive character simulation game. [Resources](https://shweetz.net/files/games/feed-snivy/resources.zip) diff --git a/cmake/copy_resources.cmake b/cmake/copy_resources.cmake index 4643944..ca38775 100644 --- a/cmake/copy_resources.cmake +++ b/cmake/copy_resources.cmake @@ -1,55 +1,41 @@ -if(NOT DEFINED SRC_DIR OR NOT DEFINED DST_DIR) - message(FATAL_ERROR "SRC_DIR and DST_DIR must be defined") +if(NOT DEFINED SRC_DIR OR NOT DEFINED DST_DIR OR NOT DEFINED STAMP_FILE) + message(FATAL_ERROR "SRC_DIR, DST_DIR, and STAMP_FILE must be defined") endif() set(CHARACTERS_DIR "${SRC_DIR}/characters") -set(CHARACTERS_ZIP_SCRIPT "${CHARACTERS_DIR}/zip") -set(IS_HOST_WINDOWS FALSE) -if(CMAKE_HOST_WIN32) - set(IS_HOST_WINDOWS TRUE) -endif() - -if(EXISTS "${CHARACTERS_ZIP_SCRIPT}" AND NOT IS_HOST_WINDOWS) - execute_process( - COMMAND "${CHARACTERS_ZIP_SCRIPT}" - WORKING_DIRECTORY "${CHARACTERS_DIR}" - RESULT_VARIABLE ZIP_SCRIPT_RESULT - ) - if(NOT ZIP_SCRIPT_RESULT EQUAL 0) - message(WARNING "Failed running ${CHARACTERS_ZIP_SCRIPT} (exit code ${ZIP_SCRIPT_RESULT}); continuing with existing archives") - endif() -endif() file(REMOVE_RECURSE "${DST_DIR}") file(MAKE_DIRECTORY "${DST_DIR}") -# Copy all resources except characters/ contents. +# Copy all resources except character source folders and any stray character zips. file(COPY "${SRC_DIR}/" DESTINATION "${DST_DIR}" - PATTERN "characters/*" EXCLUDE) + PATTERN "characters/*" EXCLUDE + PATTERN "characters/*.zip" EXCLUDE) -# Copy only .zip archives from resources/characters. file(MAKE_DIRECTORY "${DST_DIR}/characters") -file(GLOB CHARACTER_ZIPS "${CHARACTERS_DIR}/*.zip") +if(EXISTS "${CHARACTERS_DIR}") + file(GLOB CHILDREN RELATIVE "${CHARACTERS_DIR}" "${CHARACTERS_DIR}/*") + foreach(CHILD IN LISTS CHILDREN) + set(CHARACTER_SOURCE_DIR "${CHARACTERS_DIR}/${CHILD}") + if(NOT IS_DIRECTORY "${CHARACTER_SOURCE_DIR}") + continue() + endif() -if(NOT CHARACTER_ZIPS) - file(GLOB CHARACTER_FILES RELATIVE "${CHARACTERS_DIR}" "${CHARACTERS_DIR}/*") - list(FILTER CHARACTER_FILES EXCLUDE REGEX "^zip$") - list(FILTER CHARACTER_FILES EXCLUDE REGEX ".*\\.zip$") + file(GLOB_RECURSE CHARACTER_CONTENTS RELATIVE "${CHARACTER_SOURCE_DIR}" "${CHARACTER_SOURCE_DIR}/*") + if(NOT CHARACTER_CONTENTS) + message(STATUS "Skipping empty character directory: ${CHARACTER_SOURCE_DIR}") + continue() + endif() - if(CHARACTER_FILES) execute_process( - COMMAND "${CMAKE_COMMAND}" -E tar cf "snivy.zip" --format=zip ${CHARACTER_FILES} - WORKING_DIRECTORY "${CHARACTERS_DIR}" + COMMAND "${CMAKE_COMMAND}" -E tar cf "${DST_DIR}/characters/${CHILD}.zip" --format=zip ${CHARACTER_CONTENTS} + WORKING_DIRECTORY "${CHARACTER_SOURCE_DIR}" RESULT_VARIABLE ZIP_GENERATE_RESULT ) if(NOT ZIP_GENERATE_RESULT EQUAL 0) - message(WARNING "Failed generating ${CHARACTERS_DIR}/snivy.zip (exit code ${ZIP_GENERATE_RESULT}); continuing without character zip archives") - else() - file(GLOB CHARACTER_ZIPS "${CHARACTERS_DIR}/*.zip") + message(FATAL_ERROR "Failed generating ${DST_DIR}/characters/${CHILD}.zip (exit code ${ZIP_GENERATE_RESULT})") endif() - endif() + endforeach() endif() -if(CHARACTER_ZIPS) - file(COPY ${CHARACTER_ZIPS} DESTINATION "${DST_DIR}/characters") -endif() +file(WRITE "${STAMP_FILE}" "resources copied\n") diff --git a/cmake/create_index_zip.cmake b/cmake/create_index_zip.cmake index 4683078..c54d96b 100644 --- a/cmake/create_index_zip.cmake +++ b/cmake/create_index_zip.cmake @@ -2,7 +2,7 @@ if(NOT DEFINED BIN_DIR OR BIN_DIR STREQUAL "") message(FATAL_ERROR "BIN_DIR is required") endif() -set(ARCHIVE_PATH "${BIN_DIR}/snivy-web.zip") +set(ARCHIVE_PATH "${BIN_DIR}/shweets-sim-web.zip") file(REMOVE "${ARCHIVE_PATH}") file(GLOB INDEX_OUTPUTS "${BIN_DIR}/index.*") @@ -16,7 +16,7 @@ endforeach() if(FILES_TO_ZIP) execute_process( - COMMAND "${CMAKE_COMMAND}" -E tar cf "snivy-web.zip" --format=zip ${FILES_TO_ZIP} + COMMAND "${CMAKE_COMMAND}" -E tar cf "shweets-sim-web.zip" --format=zip ${FILES_TO_ZIP} WORKING_DIRECTORY "${BIN_DIR}" RESULT_VARIABLE ZIP_RESULT ) @@ -24,5 +24,5 @@ if(FILES_TO_ZIP) message(FATAL_ERROR "Failed creating ${ARCHIVE_PATH}") endif() else() - message(WARNING "No index.* files found in ${BIN_DIR}; skipping snivy-web.zip creation") + message(WARNING "No index.* files found in ${BIN_DIR}; skipping shweets-sim-web.zip creation") endif() diff --git a/snivy.rc b/shweets-sim.rc similarity index 100% rename from snivy.rc rename to shweets-sim.rc diff --git a/src/entity/actor.cpp b/src/entity/actor.cpp index c721ac5..27e3526 100644 --- a/src/entity/actor.cpp +++ b/src/entity/actor.cpp @@ -237,15 +237,23 @@ namespace game::entity if (!queuedPlay.empty()) { - auto& index = animationMap.at(queuedPlay.animation); - if (queuedPlay.isPlayAfterAnimation) - nextQueuedPlay = queuedPlay; - else if ((state == STOPPED || index != animationIndex) && currentQueuedPlay.isInterruptible) + if (!animationMap.contains(queuedPlay.animation)) { - play(queuedPlay.animation, queuedPlay.mode, queuedPlay.time, queuedPlay.speedMultiplier); - currentQueuedPlay = queuedPlay; + logger.error(std::string("Animation \"" + queuedPlay.animation + "\" does not exist! Unable to play!")); + if (!defaultAnimation.empty()) queue_default_animation(); + } + else + { + auto& index = animationMap.at(queuedPlay.animation); + if (queuedPlay.isPlayAfterAnimation) + nextQueuedPlay = queuedPlay; + else if ((state == STOPPED || index != animationIndex) && currentQueuedPlay.isInterruptible) + { + play(queuedPlay.animation, queuedPlay.mode, queuedPlay.time, queuedPlay.speedMultiplier); + currentQueuedPlay = queuedPlay; + } + queuedPlay = QueuedPlay{}; } - queuedPlay = QueuedPlay{}; } auto animation = animation_get(); diff --git a/src/entity/character.cpp b/src/entity/character.cpp index 2de063d..2427246 100644 --- a/src/entity/character.cpp +++ b/src/entity/character.cpp @@ -22,10 +22,10 @@ namespace game::entity auto& save = data.save; auto saveIsValid = save.is_valid(); - capacity = saveIsValid ? save.capacity : data.capacity; - weight = saveIsValid ? save.weight : data.weight; - digestionRate = saveIsValid ? save.digestionRate : data.digestionRate; - eatSpeed = saveIsValid ? save.eatSpeed : data.eatSpeed; + capacity = saveIsValid ? save.capacity : (double)data.capacity; + weight = saveIsValid ? save.weight : (double)data.weight; + digestionRate = saveIsValid ? save.digestionRate : (double)data.digestionRate; + eatSpeed = saveIsValid ? save.eatSpeed : (double)data.eatSpeed; calories = saveIsValid ? save.calories : 0; @@ -77,7 +77,7 @@ namespace game::entity float Character::weight_get(measurement::System system) { - return system == measurement::IMPERIAL ? weight * (float)measurement::KG_TO_LB : weight; + return system == measurement::IMPERIAL ? weight * (double)measurement::KG_TO_LB : weight; } int Character::stage_from_weight_get(float checkWeight) const @@ -99,7 +99,7 @@ namespace game::entity { if (stageIndex == -1) stageIndex = this->stage; - float threshold = data.weight; + double threshold = data.weight; if (!data.stages.empty()) { @@ -111,7 +111,7 @@ namespace game::entity threshold = data.stages[stageIndex - 1].threshold; } - return system == measurement::IMPERIAL ? threshold * (float)measurement::KG_TO_LB : threshold; + return system == measurement::IMPERIAL ? threshold * (double)measurement::KG_TO_LB : threshold; } float Character::stage_threshold_next_get(measurement::System system) const @@ -122,23 +122,35 @@ namespace game::entity float Character::stage_progress_get() { auto currentStage = stage_get(); - if (currentStage >= stage_max_get()) return 1.0f; + if (currentStage >= stage_max_get()) return 1.0; auto currentThreshold = stage_threshold_get(currentStage); auto nextThreshold = stage_threshold_get(currentStage + 1); - if (nextThreshold <= currentThreshold) return 1.0f; + if (nextThreshold <= currentThreshold) return 1.0; return (weight - currentThreshold) / (nextThreshold - currentThreshold); } - float Character::digestion_rate_get() { return digestionRate * 60; } + float Character::digestion_rate_get() { return digestionRate * 60.0; } float Character::max_capacity() const { return capacity * data.capacityMaxMultiplier; } bool Character::is_over_capacity() const { return calories > capacity; } bool Character::is_max_capacity() const { return calories >= max_capacity(); } float Character::capacity_percent_get() const { return calories / max_capacity(); } - std::string Character::animation_name_convert(const std::string& name) { return std::format("{}{}", name, stage); } + std::string Character::animation_append_id_get() const + { + if (stage <= 0) return {}; + auto stageIndex = stage - 1; + if (stageIndex < 0 || stageIndex >= (int)data.stages.size()) return {}; + return data.stages[stageIndex].animationAppendID; + } + + std::string Character::animation_name_convert(const std::string& name) + { + auto appendID = animation_append_id_get(); + return appendID.empty() ? name : name + appendID; + } void Character::play_convert(const std::string& animation, Mode playMode, float startAtTime, float speedMultiplierValue) { @@ -148,9 +160,8 @@ namespace game::entity void Character::expand_areas_apply() { auto stageProgress = stage_progress_get(); - auto capacityProgress = isDigesting - ? (float)calories / max_capacity() * (float)digestionTimer / data.digestionTimerMax - : calories / max_capacity(); + auto capacityProgress = isDigesting ? (double)calories / max_capacity() * (double)digestionTimer / data.digestionTimerMax + : calories / max_capacity(); for (int i = 0; i < (int)data.expandAreas.size(); i++) { @@ -158,8 +169,8 @@ namespace game::entity auto& overrideLayer = overrides[expandAreaOverrideLayerIDs[i]]; auto& overrideNull = overrides[expandAreaOverrideNullIDs[i]]; - auto stageScaleAdd = ((expandArea.scaleAdd * stageProgress) * 0.5f); - auto capacityScaleAdd = ((expandArea.scaleAdd * capacityProgress) * 0.5f); + auto stageScaleAdd = ((double)expandArea.scaleAdd * stageProgress) * 0.5; + auto capacityScaleAdd = ((double)expandArea.scaleAdd * capacityProgress) * 0.5; auto scaleAdd = glm::clamp(glm::vec2(), glm::vec2(stageScaleAdd + capacityScaleAdd), glm::vec2(expandArea.scaleAdd)); @@ -201,20 +212,21 @@ namespace game::entity if (digestionTimer <= 0) { auto increment = calories * data.caloriesToKilogram; + auto nextWeight = glm::clamp(weight + increment, data.weight, data.weightMax); if (is_over_capacity()) { auto capacityMaxCalorieDifference = (calories - capacity); auto overCapacityPercent = capacityMaxCalorieDifference / (max_capacity() - capacity); auto capacityIncrement = - (overCapacityPercent * data.capacityIfOverStuffedOnDigestBonus) * capacityMaxCalorieDifference; - capacity = glm::clamp(data.capacityMin, capacity + capacityIncrement, data.capacityMax); + (double)((overCapacityPercent * data.capacityIfOverStuffedOnDigestBonus) * capacityMaxCalorieDifference); + capacity = glm::clamp(capacity + capacityIncrement, (double)data.capacityMin, (double)data.capacityMax); } totalCaloriesConsumed += calories; calories = 0; - if (auto nextStage = stage_from_weight_get(weight + increment); nextStage > stage_from_weight_get(weight)) + if (auto nextStage = stage_from_weight_get((double)nextWeight); nextStage > stage_from_weight_get(weight)) { queuedPlay = QueuedPlay{}; nextQueuedPlay = QueuedPlay{}; @@ -226,7 +238,7 @@ namespace game::entity else isJustDigested = true; - weight += increment; + weight = (double)nextWeight; isDigesting = false; digestionTimer = data.digestionTimerMax; @@ -258,7 +270,7 @@ namespace game::entity auto talk_reset = [&]() { isTalking = false; - talkTimer = 0.0f; + talkTimer = 0.0; talkOverride.frame = FrameOptional(); }; @@ -277,9 +289,9 @@ namespace game::entity talkOverride.frame.size = frame.size; talkOverride.frame.pivot = frame.pivot; - talkTimer += 1.0f; + talkTimer += 1.0; - if (talkTimer > animationTalkDurations.at(animationIndex)) talkTimer = 0.0f; + if (talkTimer > animationTalkDurations.at(animationIndex)) talkTimer = 0.0; } else talk_reset(); @@ -301,7 +313,7 @@ namespace game::entity auto blink_reset = [&]() { isBlinking = false; - blinkTimer = 0.0f; + blinkTimer = 0.0; blinkOverride.frame = FrameOptional(); }; @@ -320,7 +332,7 @@ namespace game::entity blinkOverride.frame.size = frame.size; blinkOverride.frame.pivot = frame.pivot; - blinkTimer += 1.0f; + blinkTimer += 1.0; if (blinkTimer >= blinkDuration) blink_reset(); } @@ -348,14 +360,6 @@ namespace game::entity {is_over_capacity() && !data.animations.idleFull.empty() ? data.animations.idleFull : data.animations.idle}); } - void Character::queue_interact_area_animation(resource::xml::Character::InteractArea& interactArea) - { - if (isStageUp) return; - if (interactArea.animation.empty()) return; - queue_play({is_over_capacity() && !interactArea.animationFull.empty() ? interactArea.animationFull - : interactArea.animation}); - } - void Character::spritesheet_set(SpritesheetType type) { switch (type) diff --git a/src/entity/character.hpp b/src/entity/character.hpp index ee9daee..8606db0 100644 --- a/src/entity/character.hpp +++ b/src/entity/character.hpp @@ -85,9 +85,9 @@ namespace game::entity void tick(); void play_convert(const std::string&, Mode = PLAY, float time = 0.0f, float speedMultiplier = 1.0f); void queue_idle_animation(); - void queue_interact_area_animation(resource::xml::Character::InteractArea&); void queue_play(QueuedPlay); + std::string animation_append_id_get() const; std::string animation_name_convert(const std::string& name); }; } diff --git a/src/entity/item.cpp b/src/entity/item.cpp index 6a78f3d..3a40b81 100644 --- a/src/entity/item.cpp +++ b/src/entity/item.cpp @@ -10,9 +10,9 @@ using namespace glm; namespace game::entity { - Item::Item(Anm2 _anm2, glm::ivec2 _position, int _schemaID, int _chewCount, int _animationIndex, glm::vec2 _velocity, + Item::Item(Anm2 _anm2, glm::ivec2 _position, int _schemaID, int _durability, int _animationIndex, glm::vec2 _velocity, float _rotation) - : Actor(_anm2, _position, SET, 0.0f, _animationIndex), schemaID(_schemaID), chewCount(_chewCount), + : Actor(_anm2, _position, SET, 0.0f, _animationIndex), schemaID(_schemaID), durability(_durability), velocity(_velocity) { diff --git a/src/entity/item.hpp b/src/entity/item.hpp index 21b24b1..77c76e8 100644 --- a/src/entity/item.hpp +++ b/src/entity/item.hpp @@ -14,12 +14,12 @@ namespace game::entity int schemaID{}; int rotationOverrideID{}; - int chewCount{}; + int durability{}; glm::vec2 velocity{}; float angularVelocity{}; - Item(resource::xml::Anm2, glm::ivec2 position, int id, int chewCount = 0, int animationIndex = -1, + Item(resource::xml::Anm2, glm::ivec2 position, int id, int durability = 0, int animationIndex = -1, glm::vec2 velocity = {}, float rotation = 0.0f); void update(); }; diff --git a/src/loader.cpp b/src/loader.cpp index fe11a2a..dbb8565 100644 --- a/src/loader.cpp +++ b/src/loader.cpp @@ -54,7 +54,7 @@ namespace game logger.info("Initializing..."); - if (!PHYSFS_init((argc > 0 && argv && argv[0]) ? argv[0] : "snivy")) + if (!PHYSFS_init((argc > 0 && argv && argv[0]) ? argv[0] : "shweets-sim")) { logger.fatal(std::format("Failed to initialize PhysicsFS: {}", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()))); isError = true; @@ -85,12 +85,12 @@ namespace game #ifdef __EMSCRIPTEN__ static constexpr glm::vec2 SIZE = {1600, 900}; - window = SDL_CreateWindow("Feed Snivy", SIZE.x, SIZE.y, SDL_WINDOW_OPENGL); + window = SDL_CreateWindow("Shweet's Sim", SIZE.x, SIZE.y, SDL_WINDOW_OPENGL); #else SDL_PropertiesID windowProperties = SDL_CreateProperties(); - SDL_SetStringProperty(windowProperties, SDL_PROP_WINDOW_CREATE_TITLE_STRING, "Feed Snivy"); + SDL_SetStringProperty(windowProperties, SDL_PROP_WINDOW_CREATE_TITLE_STRING, "Shweet's Sim"); SDL_SetNumberProperty(windowProperties, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, (long)settings.windowSize.x); SDL_SetNumberProperty(windowProperties, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, (long)settings.windowSize.y); diff --git a/src/resource/font.hpp b/src/resource/font.hpp index d4e7dfe..ceb31e7 100644 --- a/src/resource/font.hpp +++ b/src/resource/font.hpp @@ -14,11 +14,9 @@ namespace game::resource public: static constexpr auto NORMAL = 20; - static constexpr auto ABOVE_AVERAGE = 24; - static constexpr auto BIG = 30; + static constexpr auto HEADER_1 = 24; + static constexpr auto HEADER_2 = 30; static constexpr auto HEADER_3 = 40; - static constexpr auto HEADER_2 = 50; - static constexpr auto HEADER_1 = 60; Font() = default; Font(const std::filesystem::path&, float = NORMAL); diff --git a/src/resource/xml/character.cpp b/src/resource/xml/character.cpp index 6c94ba9..33ea5b0 100644 --- a/src/resource/xml/character.cpp +++ b/src/resource/xml/character.cpp @@ -43,28 +43,31 @@ namespace game::resource::xml query_vec3(root, "ColorR", "ColorG", "ColorB", color); - root->QueryFloatAttribute("Weight", &weight); + root->QueryDoubleAttribute("Weight", &weight); + root->QueryDoubleAttribute("WeightMax", &weightMax); - root->QueryFloatAttribute("Capacity", &capacity); - root->QueryFloatAttribute("CapacityMin", &capacityMin); - root->QueryFloatAttribute("CapacityMax", &capacityMax); - root->QueryFloatAttribute("CapacityMaxMultiplier", &capacityMaxMultiplier); - root->QueryFloatAttribute("CapacityIfOverStuffedOnDigestBonus", &capacityIfOverStuffedOnDigestBonus); + root->QueryDoubleAttribute("Capacity", &capacity); + root->QueryDoubleAttribute("CapacityMin", &capacityMin); + root->QueryDoubleAttribute("CapacityMax", &capacityMax); + root->QueryDoubleAttribute("CapacityMaxMultiplier", &capacityMaxMultiplier); + root->QueryDoubleAttribute("CapacityIfOverStuffedOnDigestBonus", &capacityIfOverStuffedOnDigestBonus); - root->QueryFloatAttribute("CaloriesToKilogram", &caloriesToKilogram); + root->QueryDoubleAttribute("CaloriesToKilogram", &caloriesToKilogram); - root->QueryFloatAttribute("DigestionRate", &digestionRate); - root->QueryFloatAttribute("DigestionRateMin", &digestionRateMin); - root->QueryFloatAttribute("DigestionRateMax", &digestionRateMax); + root->QueryDoubleAttribute("DigestionRate", &digestionRate); + root->QueryDoubleAttribute("DigestionRateMin", &digestionRateMin); + root->QueryDoubleAttribute("DigestionRateMax", &digestionRateMax); root->QueryIntAttribute("DigestionTimerMax", &digestionTimerMax); - root->QueryFloatAttribute("EatSpeed", &eatSpeed); - root->QueryFloatAttribute("EatSpeedMin", &eatSpeedMin); - root->QueryFloatAttribute("EatSpeedMax", &eatSpeedMax); + root->QueryDoubleAttribute("EatSpeed", &eatSpeed); + root->QueryDoubleAttribute("EatSpeedMin", &eatSpeedMin); + root->QueryDoubleAttribute("EatSpeedMax", &eatSpeedMax); - root->QueryFloatAttribute("BlinkChance", &blinkChance); - root->QueryFloatAttribute("GurgleChance", &gurgleChance); - root->QueryFloatAttribute("GurgleCapacityMultiplier", &gurgleCapacityMultiplier); + root->QueryDoubleAttribute("BlinkChance", &blinkChance); + root->QueryDoubleAttribute("GurgleChance", &gurgleChance); + root->QueryDoubleAttribute("GurgleCapacityMultiplier", &gurgleCapacityMultiplier); + + root->QueryIntAttribute("TextBlipPeriodBase", &textBlipPeriodBase); auto dialoguePath = physfs::Path(archive + "/" + "dialogue.xml"); @@ -100,6 +103,7 @@ namespace game::resource::xml if (auto element = root->FirstChildElement("Sounds")) { + query_sound_entry_collection(element, "Blip", archive, soundRootPath, sounds.blip); query_sound_entry_collection(element, "Digest", archive, soundRootPath, sounds.digest); query_sound_entry_collection(element, "Gurgle", archive, soundRootPath, sounds.gurgle); } @@ -127,6 +131,7 @@ namespace game::resource::xml child->QueryFloatAttribute("Threshold", &stage.threshold); child->QueryIntAttribute("AreaID", &stage.areaID); dialogue.query_pool_id(child, "DialoguePoolID", stage.pool.id); + query_string_attribute(child, "AnimationAppendID", &stage.animationAppendID); stages.emplace_back(std::move(stage)); } } @@ -178,12 +183,11 @@ namespace game::resource::xml if (child->FindAttribute("Layer")) query_layer_id(child, "Layer", anm2, interactArea.layerID); query_null_id(child, "Null", anm2, interactArea.nullID); - query_string_attribute(child, "Animation", &interactArea.animation); - query_string_attribute(child, "AnimationFull", &interactArea.animationFull); query_string_attribute(child, "AnimationCursorHover", &interactArea.animationCursorHover); query_string_attribute(child, "AnimationCursorActive", &interactArea.animationCursorActive); query_sound_entry_collection(child, "Sound", archive, soundRootPath, interactArea.sound, "Path"); dialogue.query_pool_id(child, "DialoguePoolID", interactArea.pool.id); + dialogue.query_pool_id(child, "DialoguePoolIDFull", interactArea.poolFull.id); query_bool_attribute(child, "IsHold", &interactArea.isHold); child->QueryFloatAttribute("DigestionBonusRub", &interactArea.digestionBonusRub); child->QueryFloatAttribute("DigestionBonusClick", &interactArea.digestionBonusClick); @@ -225,6 +229,9 @@ namespace game::resource::xml else logger.warning(std::format("No character skill_check.xml file found: {}", path.string())); + if (auto stringsPath = physfs::Path(archive + "/" + "strings.xml"); stringsPath.is_valid()) + strings = Strings(stringsPath); + logger.info(std::format("Initialized character: {}", name)); this->path = path; diff --git a/src/resource/xml/character.hpp b/src/resource/xml/character.hpp index 0ce5622..1ce40d0 100644 --- a/src/resource/xml/character.hpp +++ b/src/resource/xml/character.hpp @@ -11,8 +11,9 @@ #include "dialogue.hpp" #include "item.hpp" #include "menu.hpp" -#include "skill_check.hpp" #include "save.hpp" +#include "skill_check.hpp" +#include "strings.hpp" namespace game::resource::xml { @@ -24,6 +25,7 @@ namespace game::resource::xml float threshold{}; int areaID{}; Dialogue::PoolReference pool{-1}; + std::string animationAppendID{}; }; struct EatArea @@ -42,8 +44,6 @@ namespace game::resource::xml struct InteractArea { - std::string animation{}; - std::string animationFull{}; std::string animationCursorActive{}; std::string animationCursorHover{}; SoundEntryCollection sound{}; @@ -53,6 +53,7 @@ namespace game::resource::xml int typeID{-1}; bool isHold{}; Dialogue::PoolReference pool{-1}; + Dialogue::PoolReference poolFull{-1}; float digestionBonusRub{}; float digestionBonusClick{}; @@ -73,6 +74,7 @@ namespace game::resource::xml struct Sounds { + SoundEntryCollection blip{}; SoundEntryCollection gurgle{}; SoundEntryCollection digest{}; }; @@ -98,6 +100,7 @@ namespace game::resource::xml Menu menuSchema{}; Cursor cursorSchema{}; SkillCheck skillCheckSchema{}; + Strings strings{}; Save save{}; @@ -119,23 +122,25 @@ namespace game::resource::xml std::string name{}; std::filesystem::path path{}; - float weight{50}; - float capacity{2000.0f}; - float capacityMin{2000.0f}; - float capacityMax{99999.0f}; - float capacityMaxMultiplier{1.5f}; - float capacityIfOverStuffedOnDigestBonus{0.25f}; - float caloriesToKilogram{1000.0f}; - float digestionRate{0.05f}; - float digestionRateMin{0.0f}; - float digestionRateMax{0.25f}; + double weight{50}; + double weightMax{1000}; + double capacity{2000.0}; + double capacityMin{2000.0}; + double capacityMax{99999.0}; + double capacityMaxMultiplier{1.5}; + double capacityIfOverStuffedOnDigestBonus{0.25}; + double caloriesToKilogram{1000.0}; + double digestionRate{0.05}; + double digestionRateMin{0.0}; + double digestionRateMax{0.25}; int digestionTimerMax{60}; - float eatSpeed{1.0f}; - float eatSpeedMin{1.0f}; - float eatSpeedMax{3.0f}; - float blinkChance{1.0f}; - float gurgleChance{1.0f}; - float gurgleCapacityMultiplier{1.0f}; + int textBlipPeriodBase{3}; + double eatSpeed{1.0}; + double eatSpeedMin{1.0}; + double eatSpeedMax{3.0}; + double blinkChance{1.0}; + double gurgleChance{1.0}; + double gurgleCapacityMultiplier{1.0}; Dialogue::PoolReference pool{-1}; Character() = default; diff --git a/src/resource/xml/character_preview.cpp b/src/resource/xml/character_preview.cpp index b3bd31e..7cb8568 100644 --- a/src/resource/xml/character_preview.cpp +++ b/src/resource/xml/character_preview.cpp @@ -41,7 +41,7 @@ namespace game::resource::xml query_string_attribute(root, "Name", &name); query_string_attribute(root, "Description", &description); - query_string_attribute(root, "Author", &author); + query_string_attribute(root, "Credits", &credits); query_vec3(root, "ColorR", "ColorG", "ColorB", color); root->QueryFloatAttribute("Weight", &weight); diff --git a/src/resource/xml/character_preview.hpp b/src/resource/xml/character_preview.hpp index b201db0..4f4c52e 100644 --- a/src/resource/xml/character_preview.hpp +++ b/src/resource/xml/character_preview.hpp @@ -28,7 +28,7 @@ namespace game::resource::xml int stages{1}; std::string name{}; - std::string author{}; + std::string credits{}; std::string description{}; std::filesystem::path path{}; float weight{50}; diff --git a/src/resource/xml/dialogue.cpp b/src/resource/xml/dialogue.cpp index bd6bc15..cf2b187 100644 --- a/src/resource/xml/dialogue.cpp +++ b/src/resource/xml/dialogue.cpp @@ -4,6 +4,7 @@ #include "../../log.hpp" #include "../../util/math.hpp" +#include #include using namespace tinyxml2; @@ -98,6 +99,8 @@ namespace game::resource::xml id++; } + + entrySelectionOrder.assign(entries.size(), -1); } if (auto element = root->FirstChildElement("Pools")) @@ -158,15 +161,32 @@ namespace game::resource::xml } } - int Dialogue::Pool::get() const + int Dialogue::entry_pick(Pool& pool) { - if (this->empty()) return -1; - auto index = rand() % this->size(); - return this->at(index); + if (pool.empty()) return -1; + + std::vector unselected{}; + for (auto id : pool) + if (id >= 0 && id < (int)entrySelectionOrder.size() && entrySelectionOrder[id] < 0) unselected.emplace_back(id); + + std::vector candidates = unselected.empty() ? std::vector(pool.begin(), pool.end()) : unselected; + if (candidates.empty()) return -1; + + auto oldestOrder = entrySelectionOrder[candidates.front()]; + for (auto id : candidates) + oldestOrder = std::min(oldestOrder, entrySelectionOrder[id]); + + std::vector oldestCandidates{}; + for (auto id : candidates) + if (entrySelectionOrder[id] == oldestOrder) oldestCandidates.emplace_back(id); + + auto pickedID = oldestCandidates.at(rand() % oldestCandidates.size()); + entrySelectionOrder[pickedID] = selectionCounter++; + return pickedID; } Dialogue::Entry* Dialogue::get(int id) { return &entries.at(id); } Dialogue::Entry* Dialogue::get(Dialogue::EntryReference& entry) { return &entries.at(entry.id); } Dialogue::Entry* Dialogue::get(const std::string& string) { return &entries.at(entryIDMap.at(string)); } - Dialogue::Entry* Dialogue::get(Dialogue::PoolReference& pool) { return &entries.at(pools.at(pool.id).get()); } - Dialogue::Entry* Dialogue::get(Dialogue::Pool& pool) { return &entries.at(pool.get()); } + Dialogue::Entry* Dialogue::get(Dialogue::PoolReference& pool) { return &entries.at(entry_pick(pools.at(pool.id))); } + Dialogue::Entry* Dialogue::get(Dialogue::Pool& pool) { return &entries.at(entry_pick(pool)); } } diff --git a/src/resource/xml/dialogue.hpp b/src/resource/xml/dialogue.hpp index 285e6c1..36d5083 100644 --- a/src/resource/xml/dialogue.hpp +++ b/src/resource/xml/dialogue.hpp @@ -5,6 +5,7 @@ #include #include +#include #include "../../util/physfs.hpp" @@ -45,11 +46,7 @@ namespace game::resource::xml inline bool is_valid() const { return id != -1; }; }; - class Pool : public std::vector - { - public: - int get() const; - }; + class Pool : public std::vector {}; std::map entryIDMap; std::map entryIDMapReverse; @@ -57,6 +54,8 @@ namespace game::resource::xml std::vector pools{}; std::map poolMap{}; + std::vector entrySelectionOrder{}; + long long selectionCounter{}; EntryReference start{-1}; EntryReference end{-1}; @@ -83,8 +82,9 @@ namespace game::resource::xml Entry* get(Dialogue::EntryReference&); Entry* get(Dialogue::Pool&); Entry* get(Dialogue::PoolReference&); + int entry_pick(Pool&); void query_entry_id(tinyxml2::XMLElement* element, const char* name, int& id); void query_pool_id(tinyxml2::XMLElement* element, const char* name, int& id); inline bool is_valid() const { return isValid; }; }; -} \ No newline at end of file +} diff --git a/src/resource/xml/item.cpp b/src/resource/xml/item.cpp index c43fa5d..c512c23 100644 --- a/src/resource/xml/item.cpp +++ b/src/resource/xml/item.cpp @@ -110,7 +110,8 @@ namespace game::resource::xml std::string itemTextureRootPath{}; query_string_attribute(element, "TextureRootPath", &itemTextureRootPath); - element->QueryIntAttribute("ChewCount", &chewCount); + element->QueryIntAttribute("Durability", &durability); + if (element->FindAttribute("ChewCount")) element->QueryIntAttribute("ChewCount", &durability); element->QueryIntAttribute("QuantityMax", &quantityMax); for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item")) @@ -121,11 +122,14 @@ namespace game::resource::xml query_string_attribute(child, "Description", &item.description); query_float_optional_attribute(child, "Calories", item.calories); + query_float_optional_attribute(child, "CapacityBonus", item.capacityBonus); + query_optional_vec3(child, "ColorR", "ColorG", "ColorB", item.color); query_float_optional_attribute(child, "DigestionBonus", item.digestionBonus); query_float_optional_attribute(child, "EatSpeedBonus", item.eatSpeedBonus); query_float_optional_attribute(child, "Gravity", item.gravity); - query_int_optional_attribute(child, "ChewCount", item.chewCount); + query_int_optional_attribute(child, "Durability", item.durability); + if (!item.durability.has_value()) query_int_optional_attribute(child, "ChewCount", item.durability); if (child->FindAttribute("UpgradeID")) { diff --git a/src/resource/xml/item.hpp b/src/resource/xml/item.hpp index 21bdc86..99b77cd 100644 --- a/src/resource/xml/item.hpp +++ b/src/resource/xml/item.hpp @@ -6,6 +6,8 @@ #include #include +#include + #include "../audio.hpp" #include "anm2.hpp" #include "sound_entry.hpp" @@ -46,10 +48,12 @@ namespace game::resource::xml std::optional upgradeID{}; std::optional flavorID; std::optional calories{}; + std::optional capacityBonus{}; + std::optional color{}; std::optional eatSpeedBonus{}; std::optional digestionBonus{}; std::optional gravity{}; - std::optional chewCount{}; + std::optional durability{}; bool isSkillCheckReward{}; bool isToggleSpritesheet{}; }; @@ -90,7 +94,7 @@ namespace game::resource::xml Animations animations{}; Sounds sounds{}; Anm2 baseAnm2{}; - int chewCount{2}; + int durability{2}; int quantityMax{99}; bool isValid{}; diff --git a/src/resource/xml/save.cpp b/src/resource/xml/save.cpp index 22479b3..4c80186 100644 --- a/src/resource/xml/save.cpp +++ b/src/resource/xml/save.cpp @@ -91,7 +91,8 @@ namespace game::resource::xml { Item item{}; child->QueryIntAttribute("ID", &item.id); - child->QueryIntAttribute("ChewCount", &item.chewCount); + child->QueryIntAttribute("Durability", &item.durability); + if (child->FindAttribute("ChewCount")) child->QueryIntAttribute("ChewCount", &item.durability); child->QueryFloatAttribute("PositionX", &item.position.x); child->QueryFloatAttribute("PositionY", &item.position.y); child->QueryFloatAttribute("VelocityX", &item.velocity.x); @@ -162,7 +163,7 @@ namespace game::resource::xml auto itemElement = itemsElement->InsertNewChildElement("Item"); itemElement->SetAttribute("ID", item.id); - itemElement->SetAttribute("ChewCount", item.chewCount); + itemElement->SetAttribute("Durability", item.durability); itemElement->SetAttribute("PositionX", item.position.x); itemElement->SetAttribute("PositionY", item.position.y); itemElement->SetAttribute("VelocityX", item.velocity.x); diff --git a/src/resource/xml/save.hpp b/src/resource/xml/save.hpp index 62c9f37..79db1c9 100644 --- a/src/resource/xml/save.hpp +++ b/src/resource/xml/save.hpp @@ -15,7 +15,7 @@ namespace game::resource::xml struct Item { int id{}; - int chewCount{}; + int durability{}; glm::vec2 position{}; glm::vec2 velocity{}; float rotation{}; diff --git a/src/resource/xml/settings.hpp b/src/resource/xml/settings.hpp index 16c2b56..630da58 100644 --- a/src/resource/xml/settings.hpp +++ b/src/resource/xml/settings.hpp @@ -23,7 +23,7 @@ namespace game::resource::xml int volume{50}; bool isUseCharacterColor{true}; - glm::vec3 color{0.120f, 0.515f, 0.115f}; + glm::vec3 color{0.120f, 0.120f, 0.120f}; glm::ivec2 windowSize{1600, 900}; glm::vec2 windowPosition{}; diff --git a/src/resource/xml/strings.cpp b/src/resource/xml/strings.cpp new file mode 100644 index 0000000..0864118 --- /dev/null +++ b/src/resource/xml/strings.cpp @@ -0,0 +1,48 @@ +#include "strings.hpp" + +#include "../../log.hpp" +#include "util.hpp" + +#include + +using namespace tinyxml2; + +namespace game::resource::xml +{ + namespace + { + std::string definition_element_name_get(const Strings::Definition& definition) + { + std::string name = definition.attribute; + if (name.rfind("Text", 0) == 0) name.replace(0, 4, "String"); + return name; + } + } + + Strings::Strings() + { + for (int i = 0; i < Count; i++) + values[i] = definitions[i].fallback; + } + + Strings::Strings(const util::physfs::Path& path) + { + for (int i = 0; i < Count; i++) + values[i] = definitions[i].fallback; + + XMLDocument document; + if (document_load(path, document) != XML_SUCCESS) return; + + auto root = document.RootElement(); + if (!root) return; + + for (int i = 0; i < Count; i++) + if (auto element = root->FirstChildElement(definition_element_name_get(definitions[i]).c_str())) + query_string_attribute(element, "Text", &values[i]); + + isValid = true; + logger.info(std::format("Initialized strings: {}", path.c_str())); + } + + const std::string& Strings::get(Type type) const { return values[type]; } +} diff --git a/src/resource/xml/strings.hpp b/src/resource/xml/strings.hpp new file mode 100644 index 0000000..64764ed --- /dev/null +++ b/src/resource/xml/strings.hpp @@ -0,0 +1,159 @@ +#pragma once + +#include "../../util/physfs.hpp" + +#include +#include + +namespace game::resource::xml +{ +#define GAME_XML_STRING_LIST(X) \ + X(MenuTabInteract, "TextMenuTabInteract", "Interact") \ + X(MenuTabArcade, "TextMenuTabArcade", "Arcade") \ + X(MenuTabInventory, "TextMenuTabInventory", "Inventory") \ + X(MenuTabSettings, "TextMenuTabSettings", "Settings") \ + X(MenuTabCheats, "TextMenuTabCheats", "Cheats") \ + X(MenuTabDebug, "TextMenuTabDebug", "Debug") \ + X(MenuOpenTooltip, "TextMenuOpenTooltip", "Open 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(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(SettingsMeasurementSystem, "TextSettingsMeasurementSystem", "Measurement System") \ + X(SettingsMetric, "TextSettingsMetric", "Metric") \ + X(SettingsMetricTooltip, "TextSettingsMetricTooltip", "Use kilograms (kg).") \ + X(SettingsImperial, "TextSettingsImperial", "Imperial") \ + X(SettingsImperialTooltip, "TextSettingsImperialTooltip", "Use pounds (lbs).") \ + X(SettingsSound, "TextSettingsSound", "Sound") \ + X(SettingsVolume, "TextSettingsVolume", "Volume") \ + X(SettingsVolumeTooltip, "TextSettingsVolumeTooltip", "Adjust master volume.") \ + X(SettingsAppearance, "TextSettingsAppearance", "Appearance") \ + X(SettingsUseCharacterColor, "TextSettingsUseCharacterColor", "Use Character Color") \ + X(SettingsUseCharacterColorTooltip, "TextSettingsUseCharacterColorTooltip", \ + "When playing, the UI will use the character's preset UI color.") \ + X(SettingsColor, "TextSettingsColor", "Color") \ + X(SettingsColorTooltip, "TextSettingsColorTooltip", "Change the UI color.") \ + X(SettingsResetButton, "TextSettingsResetButton", "Reset to Default") \ + X(SettingsSaveButton, "TextSettingsSaveButton", "Save") \ + X(SettingsSaveTooltip, "TextSettingsSaveTooltip", "Save the game.\n(Note: the game autosaves frequently.)") \ + X(SettingsReturnToCharactersButton, "TextSettingsReturnToCharactersButton", "Return to Characters") \ + X(SettingsReturnToCharactersTooltip, "TextSettingsReturnToCharactersTooltip", \ + "Go back to the character selection screen.\nProgress will be saved.") \ + X(ToastCheatsUnlocked, "TextToastCheatsUnlocked", "Cheats unlocked!") \ + X(ToastSaving, "TextToastSaving", "Saving...") \ + X(ToolsHomeButton, "TextToolsHomeButton", "Home") \ + X(ToolsHomeTooltip, "TextToolsHomeTooltip", "Reset camera view.\n(Shortcut: Home)") \ + X(ToolsOpenTooltip, "TextToolsOpenTooltip", "Open Tools") \ + X(ToolsCloseTooltip, "TextToolsCloseTooltip", "Close Tools") \ + X(DebugCursorScreenFormat, "TextDebugCursorScreenFormat", "Cursor Pos (Screen): %0.0f, %0.0f") \ + X(DebugCursorWorldFormat, "TextDebugCursorWorldFormat", "Cursor Pos (World): %0.0f, %0.0f") \ + X(DebugAnimations, "TextDebugAnimations", "Animations") \ + X(DebugNowPlayingFormat, "TextDebugNowPlayingFormat", "Now Playing: %s") \ + X(DebugDialogue, "TextDebugDialogue", "Dialogue") \ + X(DebugShowNulls, "TextDebugShowNulls", "Show Nulls (Hitboxes)") \ + X(DebugShowWorldBounds, "TextDebugShowWorldBounds", "Show World Bounds") \ + X(DebugItem, "TextDebugItem", "Item") \ + X(DebugHeld, "TextDebugHeld", "Held") \ + X(DebugItemTypeFormat, "TextDebugItemTypeFormat", "Type: %i") \ + X(DebugItemPositionFormat, "TextDebugItemPositionFormat", "Position: %0.0f, %0.0f") \ + X(DebugItemVelocityFormat, "TextDebugItemVelocityFormat", "Velocity: %0.0f, %0.0f") \ + X(DebugItemDurabilityFormat, "TextDebugItemDurabilityFormat", "Durability: %i") \ + X(InventoryEmptyHint, "TextInventoryEmptyHint", "Check the \"Arcade\" tab to earn rewards!") \ + 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(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(ArcadeSkillCheckName, "TextArcadeSkillCheckName", "Skill Check") \ + X(ArcadeSkillCheckDescription, "TextArcadeSkillCheckDescription", \ + "Test your timing to build score, chain combos, and earn rewards based on your performance.") \ + X(ArcadePlayButton, "TextArcadePlayButton", "Play") \ + X(ArcadeStatsButton, "TextArcadeStatsButton", "Stats") \ + X(ArcadeBackButton, "TextArcadeBackButton", "Back") \ + X(ArcadeBestFormat, "TextArcadeBestFormat", "Best: %i pts (%ix)") \ + X(ArcadeTotalSkillChecksFormat, "TextArcadeTotalSkillChecksFormat", "Total Skill Checks: %i") \ + X(ArcadeAccuracyFormat, "TextArcadeAccuracyFormat", "Accuracy: %0.2f%%") \ + X(InfoProgressMax, "TextInfoProgressMax", "MAX") \ + X(InfoProgressToNextStage, "TextInfoProgressToNextStage", "To Next Stage") \ + X(InfoStageProgressFormat, "TextInfoStageProgressFormat", "Stage: %i/%i (%0.1f%%)") \ + X(InfoMaxedOut, "TextInfoMaxedOut", "Maxed out!") \ + X(InfoStageStartFormat, "TextInfoStageStartFormat", "Start: %0.2f %s") \ + X(InfoStageCurrentFormat, "TextInfoStageCurrentFormat", "Current: %0.2f %s") \ + X(InfoStageNextFormat, "TextInfoStageNextFormat", "Next: %0.2f %s") \ + X(InfoDigestion, "TextInfoDigestion", "Digestion") \ + X(InfoDigesting, "TextInfoDigesting", "Digesting...") \ + 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(SkillCheckScoreFormat, "TextSkillCheckScoreFormat", "Score: %i pts (%ix)") \ + X(SkillCheckBestFormat, "TextSkillCheckBestFormat", "Best: %i pts (%ix)") \ + X(SkillCheckInstructions, "TextSkillCheckInstructions", "Match the line to the colored areas with Space/click! Better performance, better rewards!") \ + X(SkillCheckScoreLoss, "TextSkillCheckScoreLoss", "-1") \ + X(SkillCheckRewardToast, "TextSkillCheckRewardToast", "Fantastic score! Congratulations!") \ + X(SkillCheckHighScoreToast, "TextSkillCheckHighScoreToast", "High Score!") \ + X(SkillCheckGradeSuccessTemplate, "TextSkillCheckGradeSuccessTemplate", "{} (+{})") \ + X(SkillCheckMenuButton, "TextSkillCheckMenuButton", "Menu") \ + X(CheatsCalories, "TextCheatsCalories", "Calories") \ + X(CheatsCapacity, "TextCheatsCapacity", "Capacity") \ + X(CheatsWeight, "TextCheatsWeight", "Weight") \ + X(CheatsWeightFormat, "TextCheatsWeightFormat", "%0.2f kg") \ + X(CheatsStage, "TextCheatsStage", "Stage") \ + X(CheatsDigestionRate, "TextCheatsDigestionRate", "Digestion Rate") \ + X(CheatsDigestionRateFormat, "TextCheatsDigestionRateFormat", "%0.2f% / tick") \ + X(CheatsEatSpeed, "TextCheatsEatSpeed", "Eat Speed") \ + X(CheatsEatSpeedFormat, "TextCheatsEatSpeedFormat", "%0.2fx") \ + X(CheatsDigestButton, "TextCheatsDigestButton", "Digest") \ + X(CheatsInventory, "TextCheatsInventory", "Inventory") + + class Strings + { + public: + enum Type + { +#define X(type, attr, fallback) type, + GAME_XML_STRING_LIST(X) +#undef X + Count + }; + + struct Definition + { + const char* attribute; + const char* fallback; + }; + + inline static constexpr std::array definitions{{ +#define X(type, attr, fallback) {attr, fallback}, + GAME_XML_STRING_LIST(X) +#undef X + }}; + + std::array values{}; + + bool isValid{}; + + Strings(); + Strings(const util::physfs::Path&); + + const std::string& get(Type) const; + }; +} diff --git a/src/resource/xml/util.cpp b/src/resource/xml/util.cpp index ba23005..b19be8a 100644 --- a/src/resource/xml/util.cpp +++ b/src/resource/xml/util.cpp @@ -117,6 +117,29 @@ namespace game::resource::xml return result; } + XMLError query_optional_vec3(XMLElement* element, const char* attributeX, const char* attributeY, + const char* attributeZ, std::optional& value) + { + auto hasX = element->FindAttribute(attributeX); + auto hasY = element->FindAttribute(attributeY); + auto hasZ = element->FindAttribute(attributeZ); + + if (!hasX && !hasY && !hasZ) + { + value.reset(); + return XML_NO_ATTRIBUTE; + } + + value = glm::vec3(); + auto result = XML_SUCCESS; + + if (hasX) result = query_result_merge(result, element->QueryFloatAttribute(attributeX, &value->x)); + if (hasY) result = query_result_merge(result, element->QueryFloatAttribute(attributeY, &value->y)); + if (hasZ) result = query_result_merge(result, element->QueryFloatAttribute(attributeZ, &value->z)); + + return result; + } + XMLError document_load(const physfs::Path& path, XMLDocument& document) { if (!path.is_valid()) diff --git a/src/resource/xml/util.hpp b/src/resource/xml/util.hpp index b287958..9ab135e 100644 --- a/src/resource/xml/util.hpp +++ b/src/resource/xml/util.hpp @@ -32,6 +32,8 @@ namespace game::resource::xml std::optional& value); tinyxml2::XMLError query_int_optional_attribute(tinyxml2::XMLElement* element, const char* attribute, std::optional& value); + tinyxml2::XMLError query_optional_vec3(tinyxml2::XMLElement* element, const char* attributeX, const char* attributeY, + const char* attributeZ, std::optional& value); tinyxml2::XMLError query_event_id(tinyxml2::XMLElement* element, const char* name, const Anm2& anm2, int& eventID); tinyxml2::XMLError query_layer_id(tinyxml2::XMLElement* element, const char* name, const Anm2& anm2, int& layerID); diff --git a/src/state/play.cpp b/src/state/play.cpp index 8bff6ed..dfd8816 100644 --- a/src/state/play.cpp +++ b/src/state/play.cpp @@ -1,4 +1,5 @@ #include "play.hpp" +#include "play/style.hpp" #include #include @@ -11,12 +12,25 @@ #include "../util/math.hpp" using namespace game::resource; +using namespace game::resource::xml; using namespace game::util; using namespace game::state::play; using namespace glm; namespace game::state { + namespace + { + int durability_animation_index_get(const resource::xml::Item& schema, const resource::xml::Anm2& anm2, int durability, + int durabilityMax) + { + if (durability >= durabilityMax) return -1; + + auto animationName = schema.animations.chew + std::to_string(std::max(0, durability)); + return anm2.animationMap.contains(animationName) ? anm2.animationMap.at(animationName) : -1; + } + } + World::Focus Play::focus_get() { if (!isWindows) return World::CENTER; @@ -40,9 +54,10 @@ namespace game::state character = entity::Character(data, vec2(World::BOUNDS.x + World::BOUNDS.z * 0.5f, World::BOUNDS.w - World::BOUNDS.y)); - character.digestionRate = glm::clamp(data.digestionRateMin, character.digestionRate, data.digestionRateMax); - character.eatSpeed = glm::clamp(data.eatSpeedMin, character.eatSpeed, data.eatSpeedMax); - character.capacity = glm::clamp(data.capacityMin, character.capacity, data.capacityMax); + character.digestionRate = + glm::clamp(character.digestionRate, (float)data.digestionRateMin, (float)data.digestionRateMax); + character.eatSpeed = glm::clamp(character.eatSpeed, (float)data.eatSpeedMin, (float)data.eatSpeedMax); + character.capacity = glm::clamp(character.capacity, (float)data.capacityMin, (float)data.capacityMax); auto isAlternateSpritesheet = (game == NEW_GAME && math::random_percent_roll(data.alternateSpritesheet.chanceOnNewGame)); @@ -71,29 +86,34 @@ namespace game::state for (auto& item : saveData.items) { auto& anm2 = itemSchema.anm2s.at(item.id); - auto chewAnimation = itemSchema.animations.chew + std::to_string(item.chewCount); - auto animationIndex = item.chewCount > 0 ? anm2.animationMap[chewAnimation] : -1; + auto& schemaItem = itemSchema.items.at(item.id); + auto durabilityMax = schemaItem.durability.value_or(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.chewCount, animationIndex, item.velocity, + itemManager.items.emplace_back(saveItem, item.position, item.id, item.durability, animationIndex, item.velocity, item.rotation); } imgui::style::rounding_set(menuSchema.rounding); imgui::widget::sounds_set(&menuSchema.sounds.hover, &menuSchema.sounds.select); - menu.color_set_check(resources, character); + play::style::color_set(resources, character); - menu.skillCheck = SkillCheck(character); - menu.skillCheck.totalPlays = saveData.totalPlays; - menu.skillCheck.highScore = saveData.highScore; - menu.skillCheck.bestCombo = saveData.bestCombo; - menu.skillCheck.gradeCounts = saveData.gradeCounts; - menu.skillCheck.isHighScoreAchieved = saveData.highScore > 0 ? true : false; - menu.isChat = character.data.dialogue.help.is_valid() || character.data.dialogue.random.is_valid(); + menu.arcade = Arcade(character); + menu.arcade.skillCheck.totalPlays = saveData.totalPlays; + menu.arcade.skillCheck.highScore = saveData.highScore; + menu.arcade.skillCheck.bestCombo = saveData.bestCombo; + menu.arcade.skillCheck.gradeCounts = saveData.gradeCounts; + menu.arcade.skillCheck.isHighScoreAchieved = saveData.highScore > 0 ? true : false; text.entry = nullptr; text.isEnabled = false; +#if DEBUG + menu.isCheats = true; +#else menu.isCheats = false; +#endif + isPostgame = saveData.isPostgame; if (character.stage_get() >= character.stage_max_get()) isPostgame = true; if (isPostgame) menu.isCheats = true; @@ -190,7 +210,7 @@ namespace game::state menu.isCheats = true; cheatCodeIndex = 0; cheatCodeStartTime = 0.0; - toasts.push("Cheats unlocked!"); + toasts.push(character.data.strings.get(Strings::ToastCheatsUnlocked)); character.data.menuSchema.sounds.cheatsActivated.play(); } } @@ -338,10 +358,10 @@ namespace game::state save.digestionTimer = character.digestionTimer; save.totalCaloriesConsumed = character.totalCaloriesConsumed; save.totalFoodItemsEaten = character.totalFoodItemsEaten; - save.totalPlays = menu.skillCheck.totalPlays; - save.highScore = menu.skillCheck.highScore; - save.bestCombo = menu.skillCheck.bestCombo; - save.gradeCounts = menu.skillCheck.gradeCounts; + save.totalPlays = menu.arcade.skillCheck.totalPlays; + save.highScore = menu.arcade.skillCheck.highScore; + save.bestCombo = menu.arcade.skillCheck.bestCombo; + save.gradeCounts = menu.arcade.skillCheck.gradeCounts; save.isPostgame = isPostgame; save.isAlternateSpritesheet = character.spritesheetType == entity::Character::ALTERNATE; @@ -352,7 +372,7 @@ namespace game::state } for (auto& item : itemManager.items) - save.items.emplace_back(item.schemaID, item.chewCount, item.position, item.velocity, + save.items.emplace_back(item.schemaID, item.durability, item.position, item.velocity, *item.overrides[item.rotationOverrideID].frame.rotation); save.isValid = true; @@ -360,6 +380,6 @@ namespace game::state resources.character_save_set(characterIndex, save); save.serialize(character.data.save_path_get()); - toasts.push("Saving..."); + toasts.push(character.data.strings.get(Strings::ToastSaving)); } }; diff --git a/src/state/play/arcade.cpp b/src/state/play/arcade.cpp new file mode 100644 index 0000000..505980d --- /dev/null +++ b/src/state/play/arcade.cpp @@ -0,0 +1,76 @@ +#include "arcade.hpp" + +#include "../../util/imgui/widget.hpp" + +using namespace game::util::imgui; +using namespace game::resource::xml; + +namespace game::state::play +{ + Arcade::Arcade(entity::Character& character) : skillCheck(character) {} + + void Arcade::tick() { skillCheck.tick(); } + + void Arcade::update(Resources& resources, entity::Character& character, Inventory& inventory, Text& text) + { + auto available = ImGui::GetContentRegionAvail(); + auto& strings = character.data.strings; + + if (view == SKILL_CHECK) + { + if (skillCheck.update(resources, character, inventory, text)) view = MENU; + return; + } + + auto buttonHeight = ImGui::GetFrameHeightWithSpacing(); + auto childSize = ImVec2(available.x, std::max(0.0f, available.y - buttonHeight)); + + if (ImGui::BeginChild("##Arcade Child", childSize)) + { + if (view == MENU) + { + auto buttonWidth = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; + + ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_2); + ImGui::TextUnformatted(strings.get(Strings::ArcadeSkillCheckName).c_str()); + ImGui::PopFont(); + + ImGui::Separator(); + ImGui::TextWrapped("%s", strings.get(Strings::ArcadeSkillCheckDescription).c_str()); + ImGui::Separator(); + + if (WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadePlayButton).c_str(), ImVec2(buttonWidth, 0)))) + view = SKILL_CHECK; + ImGui::SameLine(); + if (WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadeStatsButton).c_str(), ImVec2(buttonWidth, 0)))) + view = SKILL_CHECK_STATS; + } + else if (view == SKILL_CHECK_STATS) + { + auto& schema = character.data.skillCheckSchema; + + ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_2); + ImGui::TextUnformatted(strings.get(Strings::ArcadeSkillCheckName).c_str()); + ImGui::PopFont(); + + ImGui::Separator(); + ImGui::Text(strings.get(Strings::ArcadeBestFormat).c_str(), skillCheck.highScore, skillCheck.bestCombo); + ImGui::Text(strings.get(Strings::ArcadeTotalSkillChecksFormat).c_str(), skillCheck.totalPlays); + + for (int i = 0; i < (int)schema.grades.size(); i++) + { + auto& grade = schema.grades[i]; + ImGui::Text("%s: %i", grade.namePlural.c_str(), skillCheck.gradeCounts[i]); + } + + ImGui::Text(strings.get(Strings::ArcadeAccuracyFormat).c_str(), skillCheck.accuracy_score_get(character)); + } + } + ImGui::EndChild(); + + if (view == SKILL_CHECK_STATS) + { + if (WIDGET_FX(ImGui::Button(strings.get(Strings::ArcadeBackButton).c_str()))) view = MENU; + } + } +} diff --git a/src/state/play/arcade.hpp b/src/state/play/arcade.hpp new file mode 100644 index 0000000..50fa575 --- /dev/null +++ b/src/state/play/arcade.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "arcade/skill_check.hpp" + +namespace game::state::play +{ + class Arcade + { + public: + enum View + { + MENU, + SKILL_CHECK, + SKILL_CHECK_STATS + }; + + SkillCheck skillCheck{}; + View view{MENU}; + + Arcade() = default; + Arcade(entity::Character&); + + void tick(); + void update(Resources&, entity::Character&, Inventory&, Text&); + }; +} diff --git a/src/state/play/arcade/skill_check.cpp b/src/state/play/arcade/skill_check.cpp index 9caeedd..dbd7b2e 100644 --- a/src/state/play/arcade/skill_check.cpp +++ b/src/state/play/arcade/skill_check.cpp @@ -6,13 +6,16 @@ #include "../../../util/imgui/widget.hpp" #include "../../../util/math.hpp" +#include #include +#include #include #include using namespace game::util; using namespace game::entity; using namespace game::resource; +using namespace game::resource::xml; using namespace glm; namespace game::state::play @@ -69,7 +72,7 @@ namespace game::state::play actor.tick(); } - void SkillCheck::update(Resources& resources, entity::Character& character, Inventory& inventory, Text& text) + bool SkillCheck::update(Resources& resources, entity::Character& character, Inventory& inventory, Text& text) { static constexpr auto BG_COLOR_MULTIPLIER = 0.5f; static constexpr ImVec4 LINE_COLOR = ImVec4(1, 1, 1, 1); @@ -82,25 +85,31 @@ namespace game::state::play auto& dialogue = character.data.dialogue; auto& schema = character.data.skillCheckSchema; auto& itemSchema = character.data.itemSchema; + auto& strings = character.data.strings; auto& style = ImGui::GetStyle(); auto drawList = ImGui::GetWindowDrawList(); auto position = ImGui::GetCursorScreenPos(); auto size = ImGui::GetContentRegionAvail(); auto spacing = ImGui::GetTextLineHeightWithSpacing(); auto& io = ImGui::GetIO(); + auto menuButtonHeight = ImGui::GetFrameHeightWithSpacing(); + size.y = std::max(0.0f, size.y - menuButtonHeight); auto cursorPos = ImGui::GetCursorPos(); - ImGui::Text("Score: %i pts (%ix)", score, combo); - auto bestString = std::format("Best: {} pts({}x)", highScore, bestCombo); + ImGui::Text(strings.get(Strings::SkillCheckScoreFormat).c_str(), score, combo); + std::array bestBuffer{}; + std::snprintf(bestBuffer.data(), bestBuffer.size(), strings.get(Strings::SkillCheckBestFormat).c_str(), highScore, + bestCombo); + auto bestString = std::string(bestBuffer.data()); ImGui::SetCursorPos(ImVec2(size.x - ImGui::CalcTextSize(bestString.c_str()).x, cursorPos.y)); - ImGui::Text("Best: %i pts (%ix)", highScore, bestCombo); + ImGui::Text(strings.get(Strings::SkillCheckBestFormat).c_str(), highScore, bestCombo); if (score == 0 && isActive) { ImGui::SetCursorPos(ImVec2(style.WindowPadding.x, size.y - style.WindowPadding.y)); - ImGui::TextWrapped("Match the line to the colored areas with Space/click! Better performance, better rewards!"); + ImGui::TextWrapped("%s", strings.get(Strings::SkillCheckInstructions).c_str()); } auto barMin = ImVec2(position.x + (size.x * 0.5f) - (spacing * 0.5f), position.y + (spacing * 2.0f)); @@ -193,8 +202,11 @@ namespace game::state::play score--; schema.sounds.scoreLoss.play(); auto toastMessagePosition = - ImVec2(barMin.x - ImGui::CalcTextSize("-1").x - ImGui::GetTextLineHeightWithSpacing(), lineMin.y); - toasts.emplace_back("-1", toastMessagePosition, schema.endTimerMax, schema.endTimerMax); + ImVec2(barMin.x - ImGui::CalcTextSize(strings.get(Strings::SkillCheckScoreLoss).c_str()).x - + ImGui::GetTextLineHeightWithSpacing(), + lineMin.y); + toasts.emplace_back(strings.get(Strings::SkillCheckScoreLoss), toastMessagePosition, schema.endTimerMax, + schema.endTimerMax); } } @@ -258,10 +270,10 @@ namespace game::state::play } auto toastMessagePosition = - ImVec2(barMin.x - ImGui::CalcTextSize("Fantastic score!\nCongratulations!").x - + ImVec2(barMin.x - ImGui::CalcTextSize(strings.get(Strings::SkillCheckRewardToast).c_str()).x - ImGui::GetTextLineHeightWithSpacing(), lineMin.y + (ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y)); - toasts.emplace_back("Fantastic score! Congratulations!", toastMessagePosition, schema.endTimerMax, + toasts.emplace_back(strings.get(Strings::SkillCheckRewardToast), toastMessagePosition, schema.endTimerMax, schema.endTimerMax); } @@ -274,9 +286,11 @@ namespace game::state::play isHighScoreAchievedThisRun = true; schema.sounds.highScore.play(); auto toastMessagePosition = - ImVec2(barMin.x - ImGui::CalcTextSize("High Score!").x - ImGui::GetTextLineHeightWithSpacing(), + ImVec2(barMin.x - ImGui::CalcTextSize(strings.get(Strings::SkillCheckHighScoreToast).c_str()).x - + ImGui::GetTextLineHeightWithSpacing(), lineMin.y + ImGui::GetTextLineHeightWithSpacing()); - toasts.emplace_back("High Score!", toastMessagePosition, schema.endTimerMax, schema.endTimerMax); + toasts.emplace_back(strings.get(Strings::SkillCheckHighScoreToast), toastMessagePosition, + schema.endTimerMax, schema.endTimerMax); } } @@ -337,10 +351,10 @@ namespace game::state::play { score = 0; combo = 0; - if (isHighScoreAchieved) schema.sounds.highScoreLoss.play(); + if (isHighScoreAchievedThisRun) schema.sounds.highScoreLoss.play(); if (highScore > 0) isHighScoreAchieved = true; isRewardScoreAchieved = false; - isHighScoreAchievedThisRun = true; + isHighScoreAchievedThisRun = false; highScoreStart = highScore; isGameOver = true; } @@ -351,7 +365,9 @@ namespace game::state::play queuedChallenge = challenge_generate(character); - auto string = grade.isFailure ? grade.name : std::format("{} (+{})", grade.name, grade.value); + auto string = grade.isFailure ? grade.name + : std::vformat(strings.get(Strings::SkillCheckGradeSuccessTemplate), + std::make_format_args(grade.name, grade.value)); auto toastMessagePosition = ImVec2(barMin.x - ImGui::CalcTextSize(string.c_str()).x - ImGui::GetTextLineHeightWithSpacing(), lineMin.y); toasts.emplace_back(string, toastMessagePosition, endTimerMax, endTimerMax); @@ -429,5 +445,8 @@ namespace game::state::play if (fallingItem.position.y > position.y + size.y) items.erase(items.begin() + i--); } ImGui::PopClipRect(); + + ImGui::SetCursorScreenPos(ImVec2(position.x, position.y + size.y + ImGui::GetStyle().ItemSpacing.y)); + return WIDGET_FX(ImGui::Button(strings.get(Strings::SkillCheckMenuButton).c_str())); } } diff --git a/src/state/play/arcade/skill_check.hpp b/src/state/play/arcade/skill_check.hpp index f64b42f..e93e3c6 100644 --- a/src/state/play/arcade/skill_check.hpp +++ b/src/state/play/arcade/skill_check.hpp @@ -81,7 +81,7 @@ namespace game::state::play SkillCheck(entity::Character&); Challenge challenge_generate(entity::Character&); void tick(); - void update(Resources&, entity::Character&, Inventory&, Text&); + bool update(Resources&, entity::Character&, Inventory&, Text&); float accuracy_score_get(entity::Character&); }; } diff --git a/src/state/play/character_manager.cpp b/src/state/play/character_manager.cpp index b6adec5..031be96 100644 --- a/src/state/play/character_manager.cpp +++ b/src/state/play/character_manager.cpp @@ -89,14 +89,18 @@ namespace game::state::play interact_area_override_tick, interactArea.scaleEffectCycles)); } - if (interactArea.pool.is_valid() && text.is_interruptible()) - text.set(dialogue.get(interactArea.pool), character); + if (text.is_interruptible()) + { + auto& pool = character.is_over_capacity() && interactArea.poolFull.is_valid() ? interactArea.poolFull + : interactArea.pool; + if (pool.is_valid()) + text.set(dialogue.get(pool), character); + } } if (isInteracting) { cursor.state = entity::Cursor::ACTION; - character.queue_interact_area_animation(interactArea); cursor.queue_play({interactArea.animationCursorActive}); if (interactArea.digestionBonusRub > 0 && character.calories > 0 && !character.isDigesting) diff --git a/src/state/play/chat.cpp b/src/state/play/chat.cpp deleted file mode 100644 index 6244500..0000000 --- a/src/state/play/chat.cpp +++ /dev/null @@ -1,32 +0,0 @@ -#include "chat.hpp" - -#include "../../util/imgui/widget.hpp" - -using namespace game::resource; -using namespace game::util::imgui; - -namespace game::state::play -{ - void Chat::update(Resources&, Text& text, entity::Character& character) - { - auto& dialogue = character.data.dialogue; - auto size = ImGui::GetContentRegionAvail(); - - ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); - - if (dialogue.random.is_valid()) - if (WIDGET_FX(ImGui::Button("Let's chat!", ImVec2(size.x, 0)))) - text.set(dialogue.get(dialogue.random), character); - - ImGui::PopFont(); - - if (dialogue.help.is_valid()) - if (WIDGET_FX(ImGui::Button("Help", ImVec2(size.x, 0)))) text.set(dialogue.get(dialogue.help), character); - - auto stage = glm::clamp(0, character.stage_get(), character.stage_max_get()); - auto& pool = stage > 0 ? character.data.stages.at(stage - 1).pool : character.data.pool; - - if (pool.is_valid()) - if (WIDGET_FX(ImGui::Button("How are you feeling?", ImVec2(size.x, 0)))) text.set(dialogue.get(pool), character); - } -} diff --git a/src/state/play/cheats.cpp b/src/state/play/cheats.cpp index a867854..2a5c2e9 100644 --- a/src/state/play/cheats.cpp +++ b/src/state/play/cheats.cpp @@ -8,82 +8,50 @@ using namespace game::util::imgui; using namespace game::util; +using namespace game::resource::xml; namespace game::state::play { - void Cheats::update(Resources&, entity::Character& character, Inventory& inventory, Text& text) + void Cheats::update(Resources&, entity::Character& character, Inventory& inventory) { - static constexpr auto FEED_INCREMENT = 100.0f; + auto& strings = character.data.strings; if (ImGui::BeginChild("##Cheats")) { - - if (WIDGET_FX(ImGui::Button("Feed"))) - { - character.calories = std::min(character.calories + FEED_INCREMENT, character.max_capacity()); - character.queue_idle_animation(); - } - ImGui::SameLine(); - if (WIDGET_FX(ImGui::Button("Starve"))) - { - character.calories = std::max(0.0f, character.calories - FEED_INCREMENT); - character.queue_idle_animation(); - } - ImGui::SameLine(); - if (WIDGET_FX(ImGui::Button("Digest"))) character.digestionProgress = entity::Character::DIGESTION_MAX; - auto stage = character.stage + 1; - if (WIDGET_FX(ImGui::SliderInt("Stage", &stage, 1, (int)character.data.stages.size() + 1))) + + auto weight_update = [&]() { character.queue_idle_animation(); }; + + WIDGET_FX(ImGui::SliderFloat(strings.get(Strings::CheatsCalories).c_str(), &character.calories, 0, + character.max_capacity(), "%0.0f kcal")); + WIDGET_FX(ImGui::SliderFloat(strings.get(Strings::CheatsCapacity).c_str(), &character.capacity, + character.data.capacityMin, character.data.capacityMax, "%0.0f kcal")); + + if (WIDGET_FX(ImGui::SliderFloat(strings.get(Strings::CheatsWeight).c_str(), &character.weight, + character.data.weight, character.data.weightMax, + strings.get(Strings::CheatsWeightFormat).c_str()))) + weight_update(); + + if (WIDGET_FX(ImGui::SliderInt(strings.get(Strings::CheatsStage).c_str(), &stage, 1, + (int)character.data.stages.size() + 1))) { character.stage = glm::clamp(0, stage - 1, (int)character.data.stages.size()); character.weight = character.stage == 0 ? character.data.weight : character.data.stages.at(character.stage - 1).threshold; - character.queue_idle_animation(); + weight_update(); } - WIDGET_FX(ImGui::SliderFloat("Capacity", &character.capacity, character.data.capacityMin, - character.data.capacityMax, "%0.0f kcal")); + WIDGET_FX(ImGui::SliderFloat(strings.get(Strings::CheatsDigestionRate).c_str(), &character.digestionRate, + character.data.digestionRateMin, character.data.digestionRateMax, + strings.get(Strings::CheatsDigestionRateFormat).c_str())); + WIDGET_FX(ImGui::SliderFloat(strings.get(Strings::CheatsEatSpeed).c_str(), &character.eatSpeed, + character.data.eatSpeedMin, character.data.eatSpeedMax, + strings.get(Strings::CheatsEatSpeedFormat).c_str())); - WIDGET_FX(ImGui::SliderFloat("Digestion Rate", &character.digestionRate, character.data.digestionRateMin, - character.data.digestionRateMax, "%0.2f% / tick")); - WIDGET_FX(ImGui::SliderFloat("Eat Speed", &character.eatSpeed, character.data.eatSpeedMin, - character.data.eatSpeedMax, "%0.2fx")); + if (WIDGET_FX(ImGui::Button(strings.get(Strings::CheatsDigestButton).c_str()))) + character.digestionProgress = entity::Character::DIGESTION_MAX; - ImGui::SeparatorText("Animations"); - ImGui::Text("Now Playing: %s", character.animationMapReverse.at(character.animationIndex).c_str()); - - auto childSize = ImVec2(0, ImGui::GetContentRegionAvail().y / 3); - - if (ImGui::BeginChild("##Animations", childSize, ImGuiChildFlags_Borders)) - { - for (int i = 0; i < (int)character.animations.size(); i++) - { - auto& animation = character.animations[i]; - ImGui::PushID(i); - if (WIDGET_FX(ImGui::Selectable(animation.name.c_str()))) - character.play(animation.name.c_str(), entity::Actor::PLAY_FORCE); - ImGui::SetItemTooltip("%s", animation.name.c_str()); - ImGui::PopID(); - } - } - ImGui::EndChild(); - - ImGui::SeparatorText("Dialogue"); - - if (ImGui::BeginChild("##Dialogue", childSize, ImGuiChildFlags_Borders)) - { - for (int i = 0; i < (int)character.data.dialogue.entries.size(); i++) - { - auto& entry = character.data.dialogue.entries[i]; - ImGui::PushID(i); - if (WIDGET_FX(ImGui::Selectable(entry.name.c_str()))) text.set(&entry, character); - ImGui::SetItemTooltip("%s", entry.name.c_str()); - ImGui::PopID(); - } - } - ImGui::EndChild(); - - ImGui::SeparatorText("Inventory"); + ImGui::SeparatorText(strings.get(Strings::CheatsInventory).c_str()); if (ImGui::BeginChild("##Inventory", ImGui::GetContentRegionAvail(), ImGuiChildFlags_Borders)) { diff --git a/src/state/play/cheats.hpp b/src/state/play/cheats.hpp index 393326a..09db361 100644 --- a/src/state/play/cheats.hpp +++ b/src/state/play/cheats.hpp @@ -10,7 +10,6 @@ namespace game::state::play class Cheats { public: - - void update(Resources&, entity::Character&, Inventory&, Text&); + void update(Resources&, entity::Character&, Inventory&); }; } diff --git a/src/state/play/debug.cpp b/src/state/play/debug.cpp index 4bd30fa..76cba28 100644 --- a/src/state/play/debug.cpp +++ b/src/state/play/debug.cpp @@ -5,31 +5,68 @@ #include using namespace game::util::imgui; +using namespace game::resource::xml; namespace game::state::play { - void Debug::update(entity::Character& character, entity::Cursor& cursor, ItemManager& itemManager, Canvas& canvas) + void Debug::update(entity::Character& character, entity::Cursor& cursor, ItemManager& itemManager, Canvas& canvas, + Text& text) { + auto& strings = character.data.strings; auto cursorPosition = canvas.screen_position_convert(cursor.position); - ImGui::Text("Cursor Pos (Screen): %0.0f, %0.0f", cursor.position.x, cursor.position.y); - ImGui::Text("Cursor Pos (World): %0.0f, %0.0f", cursorPosition.x, cursorPosition.y); + ImGui::Text(strings.get(Strings::DebugCursorScreenFormat).c_str(), cursor.position.x, cursor.position.y); + ImGui::Text(strings.get(Strings::DebugCursorWorldFormat).c_str(), cursorPosition.x, cursorPosition.y); - WIDGET_FX(ImGui::Checkbox("Show Nulls (Hitboxes)", &character.isShowNulls)); - WIDGET_FX(ImGui::Checkbox("Show World Bounds", &isBoundsDisplay)); + ImGui::SeparatorText(strings.get(Strings::DebugAnimations).c_str()); + ImGui::Text(strings.get(Strings::DebugNowPlayingFormat).c_str(), character.animationMapReverse.at(character.animationIndex).c_str()); + + auto childSize = ImVec2(0, ImGui::GetContentRegionAvail().y / 3); + + if (ImGui::BeginChild("##Animations", childSize, ImGuiChildFlags_Borders)) + { + for (int i = 0; i < (int)character.animations.size(); i++) + { + auto& animation = character.animations[i]; + ImGui::PushID(i); + if (WIDGET_FX(ImGui::Selectable(animation.name.c_str()))) + character.play(animation.name.c_str(), entity::Actor::PLAY_FORCE); + ImGui::SetItemTooltip("%s", animation.name.c_str()); + ImGui::PopID(); + } + } + ImGui::EndChild(); + + ImGui::SeparatorText(strings.get(Strings::DebugDialogue).c_str()); + + if (ImGui::BeginChild("##Dialogue", childSize, ImGuiChildFlags_Borders)) + { + for (int i = 0; i < (int)character.data.dialogue.entries.size(); i++) + { + auto& entry = character.data.dialogue.entries[i]; + ImGui::PushID(i); + if (WIDGET_FX(ImGui::Selectable(entry.name.c_str()))) text.set(&entry, character); + ImGui::SetItemTooltip("%s", entry.name.c_str()); + ImGui::PopID(); + } + } + ImGui::EndChild(); + + WIDGET_FX(ImGui::Checkbox(strings.get(Strings::DebugShowNulls).c_str(), &character.isShowNulls)); + WIDGET_FX(ImGui::Checkbox(strings.get(Strings::DebugShowWorldBounds).c_str(), &isBoundsDisplay)); if (!itemManager.items.empty()) { - ImGui::SeparatorText("Item"); + ImGui::SeparatorText(strings.get(Strings::DebugItem).c_str()); for (int i = 0; i < (int)itemManager.items.size(); i++) { auto& item = itemManager.items[i]; - if (itemManager.heldItemIndex == i) ImGui::Text("Held"); - ImGui::Text("Type: %i", item.schemaID); - ImGui::Text("Position: %0.0f, %0.0f", item.position.x, item.position.y); - ImGui::Text("Velocity: %0.0f, %0.0f", item.velocity.x, item.velocity.y); - ImGui::Text("Chew Count: %i", item.chewCount); + if (itemManager.heldItemIndex == i) ImGui::TextUnformatted(strings.get(Strings::DebugHeld).c_str()); + ImGui::Text(strings.get(Strings::DebugItemTypeFormat).c_str(), item.schemaID); + ImGui::Text(strings.get(Strings::DebugItemPositionFormat).c_str(), item.position.x, item.position.y); + ImGui::Text(strings.get(Strings::DebugItemVelocityFormat).c_str(), item.velocity.x, item.velocity.y); + ImGui::Text(strings.get(Strings::DebugItemDurabilityFormat).c_str(), item.durability); ImGui::Separator(); } } diff --git a/src/state/play/debug.hpp b/src/state/play/debug.hpp index 01d8777..bab232e 100644 --- a/src/state/play/debug.hpp +++ b/src/state/play/debug.hpp @@ -4,6 +4,7 @@ #include "../../entity/cursor.hpp" #include "item_manager.hpp" +#include "text.hpp" #include @@ -14,6 +15,6 @@ namespace game::state::play public: bool isBoundsDisplay{}; - void update(entity::Character&, entity::Cursor& cursor, ItemManager&, Canvas& canvas); + void update(entity::Character&, entity::Cursor&, ItemManager&, Canvas&, Text&); }; } diff --git a/src/state/play/info.cpp b/src/state/play/info.cpp index 4a88da4..4756ef0 100644 --- a/src/state/play/info.cpp +++ b/src/state/play/info.cpp @@ -9,6 +9,7 @@ #include using namespace game::resource; +using namespace game::resource::xml; using namespace game::util; namespace game::state::play @@ -18,6 +19,7 @@ namespace game::state::play static constexpr auto WIDTH_MULTIPLIER = 0.30f; static constexpr auto HEIGHT_MULTIPLIER = 4.0f; + auto& strings = character.data.strings; auto& style = ImGui::GetStyle(); auto windowSize = imgui::to_ivec2(ImGui::GetMainViewport()->Size); @@ -45,26 +47,29 @@ namespace game::state::play auto unitString = (system == measurement::IMPERIAL ? "lbs" : "kg"); auto weightString = util::string::format_commas(weight, 2) + " " + unitString; - ImGui::PushFont(ImGui::GetFont(), Font::ABOVE_AVERAGE); + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_1); ImGui::TextUnformatted(weightString.c_str()); ImGui::SetItemTooltip("%s", weightString.c_str()); ImGui::PopFont(); auto stageProgress = character.stage_progress_get(); ImGui::ProgressBar(stageProgress, ImVec2(ImGui::GetContentRegionAvail().x, 0), - stage >= stageMax ? "MAX" : "To Next Stage"); + strings.get(stage >= stageMax ? Strings::InfoProgressMax + : Strings::InfoProgressToNextStage) + .c_str()); if (ImGui::BeginItemTooltip()) { - ImGui::Text("Stage: %i/%i (%0.1f%%)", stage + 1, stageMax + 1, math::to_percent(stageProgress)); + ImGui::Text(strings.get(Strings::InfoStageProgressFormat).c_str(), stage + 1, stageMax + 1, + math::to_percent(stageProgress)); ImGui::Separator(); ImGui::PushStyleColor(ImGuiCol_Text, imgui::to_imvec4(color::GRAY)); if (stage >= stageMax) - ImGui::Text("Maxed out!"); + ImGui::TextUnformatted(strings.get(Strings::InfoMaxedOut).c_str()); else { - ImGui::Text("Start: %0.2f %s", stageWeight, unitString); - ImGui::Text("Current: %0.2f %s", weight, unitString); - ImGui::Text("Next: %0.2f %s", stageNextWeight, unitString); + 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(); @@ -82,7 +87,7 @@ namespace game::state::play auto overstuffedPercent = std::max(0.0f, (calories - capacity) / (character.max_capacity() - capacity)); auto caloriesColor = ImVec4(1.0f, 1.0f - overstuffedPercent, 1.0f - overstuffedPercent, 1.0f); - ImGui::PushFont(ImGui::GetFont(), Font::ABOVE_AVERAGE); + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_1); ImGui::PushStyleColor(ImGuiCol_Text, caloriesColor); auto caloriesString = std::format("{:.0f} kcal / {:.0f} kcal", calories, character.is_over_capacity() ? character.max_capacity() : character.capacity); @@ -95,14 +100,16 @@ namespace game::state::play ? (float)character.digestionTimer / character.data.digestionTimerMax : character.digestionProgress / entity::Character::DIGESTION_MAX; ImGui::ProgressBar(digestionProgress, ImVec2(ImGui::GetContentRegionAvail().x, 0), - character.isDigesting ? "Digesting..." : "Digestion"); + strings.get(character.isDigesting ? Strings::InfoDigesting + : Strings::InfoDigestion) + .c_str()); if (ImGui::BeginItemTooltip()) { if (character.isDigesting) - ImGui::TextUnformatted("Digestion in progress..."); + ImGui::TextUnformatted(strings.get(Strings::InfoDigestionInProgress).c_str()); else if (digestionProgress <= 0.0f) - ImGui::TextUnformatted("Give food to start digesting!"); + ImGui::TextUnformatted(strings.get(Strings::InfoGiveFoodToStartDigesting).c_str()); else ImGui::Text("%0.2f%%", math::to_percent(digestionProgress)); @@ -110,8 +117,8 @@ namespace game::state::play ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY))); - ImGui::Text("Rate: %0.2f%% / sec", character.digestion_rate_get()); - ImGui::Text("Eating Speed: %0.2fx", character.eatSpeed); + ImGui::Text(strings.get(Strings::InfoDigestionRateFormat).c_str(), character.digestion_rate_get()); + ImGui::Text(strings.get(Strings::InfoEatingSpeedFormat).c_str(), character.eatSpeed); ImGui::PopStyleColor(); diff --git a/src/state/play/interact.cpp b/src/state/play/interact.cpp new file mode 100644 index 0000000..939786a --- /dev/null +++ b/src/state/play/interact.cpp @@ -0,0 +1,57 @@ +#include "interact.hpp" + +#include "../../util/imgui/widget.hpp" +#include "../../util/measurement.hpp" + +using namespace game::resource; +using namespace game::resource::xml; +using namespace game::util; +using namespace game::util::imgui; + +namespace game::state::play +{ + void Interact::update(Resources& resources, Text& text, entity::Character& character) + { + auto& dialogue = character.data.dialogue; + auto& strings = character.data.strings; + auto size = ImGui::GetContentRegionAvail(); + + ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_2); + + if (dialogue.random.is_valid()) + if (WIDGET_FX(ImGui::Button(strings.get(Strings::InteractChatButton).c_str(), ImVec2(size.x, 0)))) + text.set(dialogue.get(dialogue.random), character); + + ImGui::PopFont(); + + if (dialogue.help.is_valid()) + if (WIDGET_FX(ImGui::Button(strings.get(Strings::InteractHelpButton).c_str(), ImVec2(size.x, 0)))) + text.set(dialogue.get(dialogue.help), character); + + auto stage = glm::clamp(0, character.stage_get(), character.stage_max_get()); + auto& pool = stage > 0 ? character.data.stages.at(stage - 1).pool : character.data.pool; + + if (pool.is_valid()) + if (WIDGET_FX( + ImGui::Button(strings.get(Strings::InteractFeelingButton).c_str(), ImVec2(size.x, 0)))) + text.set(dialogue.get(pool), character); + + ImGui::PushFont(ImGui::GetFont(), resource::Font::HEADER_1); + ImGui::SeparatorText(character.data.name.c_str()); + ImGui::PopFont(); + + auto& system = resources.settings.measurementSystem; + auto weight = character.weight_get(system); + auto weightUnit = system == measurement::IMPERIAL ? "lbs" : "kg"; + + ImGui::Text(strings.get(Strings::InteractWeightFormat).c_str(), weight, weightUnit, + character.stage_get() + 1); + ImGui::Text(strings.get(Strings::InteractCapacityFormat).c_str(), character.capacity, + character.max_capacity()); + ImGui::Text(strings.get(Strings::InteractDigestionRateFormat).c_str(), character.digestion_rate_get()); + ImGui::Text(strings.get(Strings::InteractEatingSpeedFormat).c_str(), character.eatSpeed); + ImGui::Separator(); + ImGui::Text(strings.get(Strings::InteractTotalCaloriesFormat).c_str(), character.totalCaloriesConsumed); + ImGui::Text(strings.get(Strings::InteractTotalFoodItemsFormat).c_str(), character.totalFoodItemsEaten); + } +} diff --git a/src/state/play/chat.hpp b/src/state/play/interact.hpp similarity index 90% rename from src/state/play/chat.hpp rename to src/state/play/interact.hpp index 78b7803..b2b1b69 100644 --- a/src/state/play/chat.hpp +++ b/src/state/play/interact.hpp @@ -6,7 +6,7 @@ namespace game::state::play { - class Chat + class Interact { public: void update(Resources&, Text&, entity::Character&); diff --git a/src/state/play/inventory.cpp b/src/state/play/inventory.cpp index 4560a0b..6f9ea2b 100644 --- a/src/state/play/inventory.cpp +++ b/src/state/play/inventory.cpp @@ -1,4 +1,5 @@ #include "inventory.hpp" +#include "style.hpp" #include #include @@ -7,6 +8,7 @@ #include "../../util/color.hpp" #include "../../util/imgui.hpp" +#include "../../util/imgui/style.hpp" #include "../../util/imgui/widget.hpp" #include "../../util/math.hpp" @@ -18,6 +20,8 @@ using namespace glm; namespace game::state::play { + using Strings = resource::xml::Strings; + void Inventory::tick() { for (auto& [i, actor] : actors) @@ -29,6 +33,7 @@ namespace game::state::play static constexpr auto INFO_CHILD_HEIGHT_MULTIPLIER = 1.0f / 3.0f; auto& schema = character.data.itemSchema; + auto& strings = character.data.strings; auto quantity_get = [&](int itemID) -> int& { @@ -46,6 +51,77 @@ namespace game::state::play auto is_able_to_upgrade_get = [&](const resource::xml::Item::Entry& item, int quantity) { return is_possible_to_upgrade_get(item) && quantity >= *item.upgradeCount; }; + auto item_summary_draw = [&](const resource::xml::Item::Entry& item, int quantity) + { + auto& category = schema.categories[item.categoryID]; + auto& rarity = schema.rarities[item.rarityID]; + auto durability = item.durability.value_or(schema.durability); + + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); + ImGui::TextWrapped("%s (x%i)", item.name.c_str(), quantity); + ImGui::PopFont(); + + ImGui::Separator(); + 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.flavorID.has_value()) + ImGui::TextWrapped(strings.get(Strings::InventoryFlavorFormat).c_str(), + schema.flavors[*item.flavorID].name.c_str()); + if (item.calories.has_value()) + ImGui::TextWrapped(strings.get(Strings::InventoryCaloriesFormat).c_str(), *item.calories); + ImGui::TextWrapped(strings.get(Strings::InventoryDurabilityFormat).c_str(), durability); + if (item.capacityBonus.has_value()) + ImGui::TextWrapped(strings.get(Strings::InventoryCapacityBonusFormat).c_str(), *item.capacityBonus); + if (item.digestionBonus.has_value()) + { + 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.eatSpeedBonus.has_value()) + { + 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(); + + ImGui::Separator(); + }; + + auto item_details_draw = [&](const resource::xml::Item::Entry& item, int quantity) + { + item_summary_draw(item, quantity); + + if (ImGui::BeginChild("##Info Description Child", ImGui::GetContentRegionAvail())) + ImGui::TextWrapped("%s", item.description.c_str()); + ImGui::EndChild(); + }; + + auto item_tooltip_draw = [&](const resource::xml::Item::Entry& item, int quantity) + { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 24.0f); + item_summary_draw(item, quantity); + ImGui::TextWrapped("%s", item.description.c_str()); + 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::PopFont(); + }; + auto item_use = [&](int itemID) { auto& item = schema.items[itemID]; @@ -176,6 +252,7 @@ namespace game::state::play auto& item = schema.items[i]; auto& quantity = quantity_get(i); auto& rarity = schema.rarities[item.rarityID]; + auto hasItemColor = item.color.has_value(); if (rarity.isHidden && quantity <= 0) continue; @@ -185,6 +262,7 @@ namespace game::state::play auto cursorScreenPos = ImGui::GetCursorScreenPos(); auto [canvas, rect] = item_canvas_get(i, size); auto isSelected = selectedItemID == i; + if (hasItemColor) imgui::style::color_set(*item.color); if (isSelected) { @@ -201,8 +279,16 @@ namespace game::state::play isAnyInventoryItemHovered = isAnyInventoryItemHovered || ImGui::IsItemHovered(); if (isPressed) selectedItemID = i; if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && quantity > 0) item_use(i); + if (ImGui::BeginItemTooltip()) + { + if (quantity > 0) + item_tooltip_draw(item, quantity); + else + item_unknown_draw(); + ImGui::EndTooltip(); + } - ImGui::PushFont(ImGui::GetFont(), Font::BIG); + 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, @@ -210,6 +296,7 @@ namespace game::state::play ImGui::GetWindowDrawList()->AddText(textPos, ImGui::GetColorU32(ImGui::GetStyleColorVec4(ImGuiCol_Text)), text.c_str()); ImGui::PopFont(); + if (hasItemColor) style::color_set(resources, character); auto increment = ImGui::GetItemRectSize().x + ImGui::GetStyle().ItemSpacing.x; cursorPos.x += increment; @@ -231,13 +318,15 @@ namespace game::state::play isItemSelected = selectedItemID >= 0 && selectedItemID < (int)schema.items.size(); auto selectedQuantity = isItemSelected ? quantity_get(selectedItemID) : 0; auto isSelectedItemKnown = isItemSelected && selectedQuantity > 0; + auto selectedItemHasColor = isItemSelected && schema.items[selectedItemID].color.has_value(); if (isInfoVisible && ImGui::BeginChild("##Info Child", infoChildSize, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar)) { + if (selectedItemHasColor) imgui::style::color_set(*schema.items[selectedItemID].color); ImGui::Separator(); auto isButtonChildVisible = selectedQuantity > 0; - ImGui::PushFont(resources.font.get(), Font::BIG); + 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; @@ -252,56 +341,18 @@ namespace game::state::play { if (!isItemSelected) { - ImGui::PushFont(ImGui::GetFont(), Font::BIG); - ImGui::TextWrapped("%s", "Check the \"Arcade\" tab to earn rewards!"); + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); + ImGui::TextWrapped("%s", strings.get(Strings::InventoryEmptyHint).c_str()); ImGui::PopFont(); } else { auto& item = schema.items[selectedItemID]; - auto& category = schema.categories[item.categoryID]; - auto& rarity = schema.rarities[item.rarityID]; if (isSelectedItemKnown) - { - ImGui::PushFont(ImGui::GetFont(), Font::BIG); - ImGui::TextWrapped("%s (x%i)", item.name.c_str(), selectedQuantity); - ImGui::PopFont(); - - ImGui::Separator(); - 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.flavorID.has_value()) - ImGui::TextWrapped("Flavor: %s", schema.flavors[*item.flavorID].name.c_str()); - if (item.calories.has_value()) ImGui::TextWrapped("%0.0f kcal", *item.calories); - if (item.digestionBonus.has_value()) - { - if (*item.digestionBonus > 0) - ImGui::TextWrapped("Digestion Rate Bonus: +%0.2f%% / sec", *item.digestionBonus * 60.0f); - else if (*item.digestionBonus < 0) - ImGui::TextWrapped("Digestion Rate Penalty: %0.2f%% / sec", *item.digestionBonus * 60.0f); - } - if (item.eatSpeedBonus.has_value()) - { - if (*item.eatSpeedBonus > 0) - ImGui::TextWrapped("Eat Speed Bonus: +%0.2f%% / sec", *item.eatSpeedBonus); - else if (*item.eatSpeedBonus < 0) - ImGui::TextWrapped("Eat Speed Penalty: %0.2f%% / sec", *item.eatSpeedBonus); - } - if (is_possible_to_upgrade_get(item)) - ImGui::TextWrapped("Upgrade: %ix -> %s", *item.upgradeCount, - schema.idToStringMap.at(*item.upgradeID).c_str()); - ImGui::PopStyleColor(); - - ImGui::Separator(); - ImGui::TextWrapped("%s", item.description.c_str()); - } + item_details_draw(item, selectedQuantity); else - { - ImGui::PushFont(ImGui::GetFont(), Font::BIG); - ImGui::TextWrapped("%s", "???"); - ImGui::PopFont(); - } + item_unknown_draw(); } } ImGui::EndChild(); @@ -310,28 +361,75 @@ namespace game::state::play ImGui::BeginChild("##Info Actions Child", buttonChildSize, ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar)) { + auto& selectedItem = schema.items[selectedItemID]; auto canUseSelectedItem = true; - auto canUpgradeSelectedItem = is_able_to_upgrade_get(schema.items[selectedItemID], selectedQuantity); + auto canUpgradeSelectedItem = is_able_to_upgrade_get(selectedItem, selectedQuantity); auto rowTwoButtonSize = row_widget_size_get(2); + auto upgrade_item_name_get = [&]() -> std::string + { + if (!selectedItem.upgradeID.has_value()) return {}; + return schema.items.at(*selectedItem.upgradeID).name; + }; + + 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::Separator(); ImGui::Dummy(ImVec2(0, ImGui::GetStyle().ItemSpacing.y)); - ImGui::PushFont(ImGui::GetFont(), Font::BIG); + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); ImGui::BeginDisabled(!canUseSelectedItem); - if (WIDGET_FX(ImGui::Button("Spawn", {ImGui::GetContentRegionAvail().x, 0}))) item_use(selectedItemID); + 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("Upgrade", rowTwoButtonSize))) item_upgrade(selectedItemID, false); + if (WIDGET_FX( + ImGui::Button(strings.get(Strings::InventoryUpgradeButton).c_str(), rowTwoButtonSize))) + item_upgrade(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("Upgrade All", rowTwoButtonSize))) item_upgrade(selectedItemID, true); + if (WIDGET_FX(ImGui::Button(strings.get(Strings::InventoryUpgradeAllButton).c_str(), + rowTwoButtonSize))) + item_upgrade(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 (isButtonChildVisible) ImGui::EndChild(); + if (selectedItemHasColor) style::color_set(resources, character); } if (isInfoVisible) ImGui::EndChild(); } diff --git a/src/state/play/item_manager.cpp b/src/state/play/item_manager.cpp index 1115151..b5fdbd5 100644 --- a/src/state/play/item_manager.cpp +++ b/src/state/play/item_manager.cpp @@ -15,6 +15,18 @@ using namespace glm; namespace game::state::play { + namespace + { + int durability_animation_index_get(const resource::xml::Item& schema, const resource::xml::Anm2& anm2, int durability, + int durabilityMax) + { + if (durability >= durabilityMax) return -1; + + auto animationName = schema.animations.chew + std::to_string(std::max(0, durability)); + return anm2.animationMap.contains(animationName) ? anm2.animationMap.at(animationName) : -1; + } + } + void ItemManager::update(entity::Character& character, entity::Cursor& cursor, AreaManager& areaManager, Text& text, const glm::vec4& bounds, Canvas& canvas) { @@ -70,7 +82,11 @@ namespace game::state::play math::random_in_range(spawnBounds.y, spawnBounds.y + spawnBounds.w)); auto& itemSchema = character.data.itemSchema; - items.emplace_back(itemSchema.anm2s.at(id), position, id); + auto& item = itemSchema.items.at(id); + auto& anm2 = itemSchema.anm2s.at(id); + auto durabilityMax = item.durability.value_or(itemSchema.durability); + auto animationIndex = durability_animation_index_get(itemSchema, anm2, 0, durabilityMax); + items.emplace_back(anm2, position, id, 0, animationIndex); } queuedItemIDs.clear(); @@ -97,15 +113,15 @@ namespace game::state::play if (schema.categories[item.categoryID].isEdible) { - auto& chewCountMax = item.chewCount.has_value() ? *item.chewCount : schema.chewCount; - auto caloriesChew = item.calories.has_value() ? *item.calories / (chewCountMax + 1) : 0; - auto isCanEat = character.calories + caloriesChew <= character.max_capacity(); + auto& durabilityMax = item.durability.has_value() ? *item.durability : schema.durability; + auto caloriesPerBite = item.calories.has_value() && durabilityMax > 0 ? *item.calories / durabilityMax : 0; + auto isCanEat = character.calories + caloriesPerBite <= character.max_capacity(); if (isJustItemHeld) { if (isCanEat) text.set(dialogue.get(isOverCapacity ? dialogue.feedFull : dialogue.feed), character); - else if (caloriesChew > character.capacity) + else if (caloriesPerBite > character.capacity) text.set(dialogue.get(dialogue.lowCapacity), character); else text.set(dialogue.get(dialogue.full), character); @@ -125,37 +141,44 @@ namespace game::state::play if (character.playedEventID == eatArea.eventID) { - heldItem->chewCount++; + heldItem->durability++; character.consume_played_event(); - auto chewAnimation = schema.animations.chew + std::to_string(heldItem->chewCount); - auto animationIndex = heldItem->chewCount > 0 ? heldItem->animationMap[chewAnimation] : -1; - - heldItem->play(animationIndex, entity::Actor::SET); - - character.calories += caloriesChew; - character.totalCaloriesConsumed += caloriesChew; + character.calories += caloriesPerBite; + character.totalCaloriesConsumed += caloriesPerBite; + if (item.capacityBonus.has_value()) + { + character.capacity += *item.capacityBonus / durabilityMax; + character.capacity = + glm::clamp(character.capacity, (float)character.data.capacityMin, (float)character.data.capacityMax); + } if (item.eatSpeedBonus.has_value()) { - character.eatSpeed += *item.eatSpeedBonus / (chewCountMax + 1); - character.eatSpeed = - glm::clamp(character.data.eatSpeedMin, character.eatSpeed, character.data.eatSpeedMax); + character.eatSpeed += *item.eatSpeedBonus / durabilityMax; + character.eatSpeed = glm::clamp(character.eatSpeed, (float)character.data.eatSpeedMin, + (float)character.data.eatSpeedMax); } if (item.digestionBonus.has_value()) { - character.digestionRate += *item.digestionBonus / (chewCountMax + 1); - character.digestionRate = glm::clamp(character.data.digestionRateMin, character.digestionRate, - character.data.digestionRateMax); + character.digestionRate += *item.digestionBonus / durabilityMax; + character.digestionRate = glm::clamp(character.digestionRate, (float)character.data.digestionRateMin, + (float)character.data.digestionRateMax); } - if (heldItem->chewCount > chewCountMax) + if (heldItem->durability >= durabilityMax) { isQueueFinishFood = true; character.totalFoodItemsEaten++; queuedRemoveItemIndex = heldItemIndex; heldItemIndex = -1; } + else + { + auto animationIndex = + durability_animation_index_get(schema, *heldItem, heldItem->durability, durabilityMax); + heldItem->play(animationIndex, entity::Actor::SET); + } } } @@ -177,7 +200,7 @@ namespace game::state::play // Food stolen if (auto animation = character.animation_get(character.animation_name_convert(eatArea.animation)); - character.is_playing(animation->name)) + animation && character.is_playing(animation->name)) { if (!math::is_point_in_rectf(rect, heldItem->position)) text.set(dialogue.get(isOverCapacity ? dialogue.foodTakenFull : dialogue.foodTaken), character); @@ -232,7 +255,7 @@ namespace game::state::play if (isMouseRightClicked) { - if (item.chewCount > 0) + if (item.durability > 0) schema.sounds.dispose.play(); else { diff --git a/src/state/play/menu.cpp b/src/state/play/menu.cpp index 71a2035..7f5ae8e 100644 --- a/src/state/play/menu.cpp +++ b/src/state/play/menu.cpp @@ -1,5 +1,7 @@ #include "menu.hpp" +#include "style.hpp" + #include "../../util/imgui.hpp" #include "../../util/imgui/style.hpp" #include "../../util/imgui/widget.hpp" @@ -8,18 +10,14 @@ using namespace game::util; using namespace game::util::imgui; +using namespace game::resource::xml; namespace game::state::play { void Menu::tick() { inventory.tick(); - skillCheck.tick(); - } - - void Menu::color_set_check(Resources& resources, entity::Character& character) - { - imgui::style::color_set(resources.settings.isUseCharacterColor ? character.data.color : resources.settings.color); + arcade.tick(); } void Menu::update(Resources& resources, ItemManager& itemManager, entity::Character& character, @@ -28,6 +26,7 @@ namespace game::state::play static constexpr auto WIDTH_MULTIPLIER = 0.30f; auto& schema = character.data.menuSchema; + auto& strings = character.data.strings; auto style = ImGui::GetStyle(); auto& io = ImGui::GetIO(); @@ -61,47 +60,41 @@ namespace game::state::play if (ImGui::BeginTabBar("##Options")) { - if (isChat && WIDGET_FX(ImGui::BeginTabItem("Chat"))) + if (WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabInteract).c_str()))) { - chat.update(resources, text, character); + interact.update(resources, text, character); ImGui::EndTabItem(); } - if (WIDGET_FX(ImGui::BeginTabItem("Arcade"))) + if (WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabArcade).c_str()))) { - skillCheck.update(resources, character, inventory, text); + arcade.update(resources, character, inventory, text); ImGui::EndTabItem(); } - if (WIDGET_FX(ImGui::BeginTabItem("Inventory"))) + if (WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabInventory).c_str()))) { inventory.update(resources, itemManager, character); ImGui::EndTabItem(); } - if (WIDGET_FX(ImGui::BeginTabItem("Stats"))) + if (WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabSettings).c_str()))) { - stats.update(resources, skillCheck, character); + settingsMenu.update(resources, SettingsMenu::PLAY, &strings); + if (settingsMenu.isJustColorSet) style::color_set(resources, character); ImGui::EndTabItem(); } - if (WIDGET_FX(ImGui::BeginTabItem("Settings"))) + if (isCheats && WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabCheats).c_str()))) { - settingsMenu.update(resources, SettingsMenu::PLAY); - if (settingsMenu.isJustColorSet) color_set_check(resources, character); - ImGui::EndTabItem(); - } - - if (isCheats && WIDGET_FX(ImGui::BeginTabItem("Cheats"))) - { - cheats.update(resources, character, inventory, text); + cheats.update(resources, character, inventory); ImGui::EndTabItem(); } #if DEBUG - if (WIDGET_FX(ImGui::BeginTabItem("Debug"))) + if (WIDGET_FX(ImGui::BeginTabItem(strings.get(Strings::MenuTabDebug).c_str()))) { - debug.update(character, cursor, itemManager, canvas); + debug.update(character, cursor, itemManager, canvas, text); ImGui::EndTabItem(); } #endif @@ -128,7 +121,9 @@ namespace game::state::play if (t <= 0.0f || t >= 1.0f) { - ImGui::SetItemTooltip(isOpen ? "Close Main Menu" : "Open Main Menu"); + ImGui::SetItemTooltip("%s", strings.get(isOpen ? Strings::MenuCloseTooltip + : Strings::MenuOpenTooltip) + .c_str()); if (result) { isOpen = !isOpen; diff --git a/src/state/play/menu.hpp b/src/state/play/menu.hpp index f4cb32f..76b5306 100644 --- a/src/state/play/menu.hpp +++ b/src/state/play/menu.hpp @@ -4,11 +4,11 @@ #include "../settings_menu.hpp" -#include "arcade/skill_check.hpp" -#include "chat.hpp" +#include "arcade.hpp" #include "cheats.hpp" #include "debug.hpp" -#include "stats.hpp" +#include "interact.hpp" +#include "inventory.hpp" #include "text.hpp" #include "../../util/imgui/window_slide.hpp" @@ -18,27 +18,24 @@ namespace game::state::play class Menu { public: - SkillCheck skillCheck; - Chat chat; + Arcade arcade; + Interact interact; Cheats cheats; Debug debug; - Stats stats; Inventory inventory; state::SettingsMenu settingsMenu; #if DEBUG bool isCheats{true}; -#elif +#else bool isCheats{}; #endif bool isOpen{true}; - bool isChat{true}; util::imgui::WindowSlide slide{}; void tick(); void update(Resources&, ItemManager&, entity::Character&, entity::Cursor&, Text&, Canvas&); - void color_set_check(Resources&, entity::Character&); }; } diff --git a/src/state/play/stats.cpp b/src/state/play/stats.cpp deleted file mode 100644 index 7857900..0000000 --- a/src/state/play/stats.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#include "stats.hpp" - -#include - -#include "../../util/measurement.hpp" - -using namespace game::resource; -using namespace game::util; - -namespace game::state::play -{ - void Stats::update(Resources& resources, SkillCheck& skillCheck, entity::Character& character) - { - ImGui::PushFont(ImGui::GetFont(), Font::BIG); - ImGui::TextUnformatted(character.data.name.c_str()); - ImGui::PopFont(); - - ImGui::Separator(); - - auto& skillCheckSchema = character.data.skillCheckSchema; - auto& system = resources.settings.measurementSystem; - auto weight = character.weight_get(system); - auto weightUnit = system == measurement::IMPERIAL ? "lbs" : "kg"; - - ImGui::Text("Weight: %0.2f %s (Stage: %i)", weight, weightUnit, character.stage_get() + 1); - ImGui::Text("Capacity: %0.0f kcal (Max: %0.0f kcal)", character.capacity, character.max_capacity()); - ImGui::Text("Digestion Rate: %0.2f%%/sec", character.digestion_rate_get()); - ImGui::Text("Eating Speed: %0.2fx", character.eatSpeed); - - ImGui::SeparatorText("Totals"); - - ImGui::Text("Total Calories Consumed: %0.0f kcal", character.totalCaloriesConsumed); - ImGui::Text("Total Food Items Eaten: %i", character.totalFoodItemsEaten); - - ImGui::SeparatorText("Skill Check"); - - ImGui::Text("Best: %i pts (%ix)", skillCheck.highScore, skillCheck.bestCombo); - ImGui::Text("Total Skill Checks: %i", skillCheck.totalPlays); - - for (int i = 0; i < (int)skillCheckSchema.grades.size(); i++) - { - auto& grade = skillCheckSchema.grades[i]; - ImGui::Text("%s: %i", grade.namePlural.c_str(), skillCheck.gradeCounts[i]); - } - - ImGui::Text("Accuracy: %0.2f%%", skillCheck.accuracy_score_get(character)); - } -} diff --git a/src/state/play/stats.hpp b/src/state/play/stats.hpp deleted file mode 100644 index e3b7c55..0000000 --- a/src/state/play/stats.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include "../../entity/character.hpp" -#include "../../resources.hpp" - -#include "arcade/skill_check.hpp" - -#include - -namespace game::state::play -{ - class Stats - { - public: - void update(Resources&, SkillCheck&, entity::Character&); - }; -} diff --git a/src/state/play/style.hpp b/src/state/play/style.hpp new file mode 100644 index 0000000..cfdebe5 --- /dev/null +++ b/src/state/play/style.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "../../entity/character.hpp" +#include "../../resources.hpp" +#include "../../util/imgui/style.hpp" + +namespace game::state::play::style +{ + inline void color_set(Resources& resources, const entity::Character& character) + { + game::util::imgui::style::color_set(resources.settings.isUseCharacterColor ? character.data.color + : resources.settings.color); + } +} diff --git a/src/state/play/text.cpp b/src/state/play/text.cpp index 1ddd0f6..d36bf98 100644 --- a/src/state/play/text.cpp +++ b/src/state/play/text.cpp @@ -37,10 +37,12 @@ namespace game::state::play index = 0; time = 0.0f; isEnabled = true; - character.isTalking = true; if (!dialogueEntry->animation.empty()) character.queue_play({.animation = dialogueEntry->animation, .isInterruptible = isInterruptible}); - if (dialogueEntry->text.empty()) isEnabled = false; + if (dialogueEntry->text.empty()) + isEnabled = false; + else + character.isTalking = true; } void Text::tick(entity::Character& character) @@ -48,6 +50,8 @@ namespace game::state::play if (!entry || isFinished) return; index++; + auto blipPeriod = character.data.textBlipPeriodBase; + if (blipPeriod > 0 && index % blipPeriod == 0) character.data.sounds.blip.play(); if (index >= ImTextCountCharsFromUtf8(entry->text.c_str(), entry->text.c_str() + entry->text.size())) { diff --git a/src/state/play/tools.cpp b/src/state/play/tools.cpp index 09db70b..ad423d2 100644 --- a/src/state/play/tools.cpp +++ b/src/state/play/tools.cpp @@ -7,6 +7,7 @@ using namespace game::util; using namespace game::util::imgui; +using namespace game::resource::xml; namespace game::state::play { @@ -18,6 +19,7 @@ namespace game::state::play auto style = ImGui::GetStyle(); auto& io = ImGui::GetIO(); auto& schema = character.data.menuSchema; + auto& strings = character.data.strings; slide.update(isOpen, io.DeltaTime); @@ -56,8 +58,9 @@ namespace game::state::play ImGui::PopStyleColor(); }; - if (WIDGET_FX(ImGui::Button("Home", buttonSize))) world.character_focus(character, canvas, focus); - ImGui::SetItemTooltip("%s", "Reset camera view.\n(Shortcut: Home)"); + if (WIDGET_FX(ImGui::Button(strings.get(Strings::ToolsHomeButton).c_str(), buttonSize))) + world.character_focus(character, canvas, focus); + ImGui::SetItemTooltip("%s", strings.get(Strings::ToolsHomeTooltip).c_str()); for (int i = 0; i < (int)character.data.interactTypeNames.size(); i++) cursor_mode_button(character.data.interactTypeNames[i], i); @@ -82,7 +85,9 @@ namespace game::state::play if (t <= 0.0f || t >= 1.0f) { - ImGui::SetItemTooltip(isOpen ? "Close Tools" : "Open Tools"); + ImGui::SetItemTooltip("%s", strings.get(isOpen ? Strings::ToolsCloseTooltip + : Strings::ToolsOpenTooltip) + .c_str()); if (result) { isOpen = !isOpen; diff --git a/src/state/play/world.cpp b/src/state/play/world.cpp index 62dd594..67059e2 100644 --- a/src/state/play/world.cpp +++ b/src/state/play/world.cpp @@ -62,6 +62,7 @@ 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(); @@ -72,13 +73,18 @@ namespace game::state::play rect = {rect.x - PADDING * 0.5f, rect.y - PADDING * 0.5f, rect.z + PADDING, rect.w + PADDING}; - auto zoomFactor = std::min((float)canvas.size.x / rect.z, (float)canvas.size.y / rect.w); + 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); canvas.zoom = glm::clamp(ZOOM_MIN, math::to_percent(zoomFactor), ZOOM_MAX); 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; - canvas.pan = rectCenter - (vec2(viewSizeWorld.x, viewSizeWorld.y) * 0.5f); + auto infoHeightWorld = infoHeightPixels / zoomFactor; + canvas.pan = rectCenter - vec2(viewSizeWorld.x * 0.5f, (viewSizeWorld.y + infoHeightWorld) * 0.5f); auto menuWidthWorld = (canvas.size.x * MENU_WIDTH_MULTIPLIER) / zoomFactor; auto toolsWidthWorld = (canvas.size.x * TOOLS_WIDTH_MULTIPLIER) / zoomFactor; diff --git a/src/state/select/info.cpp b/src/state/select/info.cpp index 0756af6..6307e84 100644 --- a/src/state/select/info.cpp +++ b/src/state/select/info.cpp @@ -47,38 +47,51 @@ namespace game::state::select ImGui::TextUnformatted(character.name.c_str()); ImGui::PopFont(); - if (!character.description.empty()) - { - ImGui::Separator(); - - ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY))); - ImGui::PushFont(ImGui::GetFont(), Font::BIG); - ImGui::TextWrapped("%s", character.description.c_str()); - ImGui::PopFont(); - - ImGui::PopStyleColor(); - } - - ImGui::Separator(); - - ImGui::PushFont(ImGui::GetFont(), Font::BIG); - - ImGui::Text("Weight: %0.2f %s", system == IMPERIAL ? weight * KG_TO_LB : weight, - system == IMPERIAL ? "lbs" : "kg"); - ImGui::Text("Stages: %i", character.stages); - - ImGui::Separator(); - - ImGui::PopFont(); - ImGui::PushFont(ImGui::GetFont(), Font::NORMAL); + if (ImGui::BeginTabBar("##Preview Tabs")) + { + if (ImGui::BeginTabItem("Overview")) + { + if (!character.description.empty()) + { + ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY))); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY))); + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); + ImGui::TextWrapped("%s", character.description.c_str()); + ImGui::PopFont(); - if (!character.author.empty()) ImGui::TextWrapped("Author: %s", character.author.c_str()); + ImGui::PopStyleColor(); + } - ImGui::PopStyleColor(); + ImGui::Separator(); + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); + + ImGui::Text("Weight: %0.2f %s", system == IMPERIAL ? weight * KG_TO_LB : weight, + system == IMPERIAL ? "lbs" : "kg"); + ImGui::Text("Stages: %i", character.stages); + + ImGui::PopFont(); + ImGui::EndTabItem(); + } + + if (ImGui::BeginTabItem("Credits")) + { + ImGui::Separator(); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(imgui::to_imvec4(color::GRAY))); + ImGui::PushFont(ImGui::GetFont(), Font::HEADER_2); + if (!character.credits.empty()) + ImGui::TextWrapped("%s", character.credits.c_str()); + else + ImGui::TextUnformatted("No credits listed."); + ImGui::PopFont(); + ImGui::PopStyleColor(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } ImGui::PopFont(); } ImGui::EndChild(); diff --git a/src/state/select/preview.cpp b/src/state/select/preview.cpp index a95aa24..e691c5e 100644 --- a/src/state/select/preview.cpp +++ b/src/state/select/preview.cpp @@ -54,7 +54,7 @@ namespace game::state::select auto renderSize = ImVec2(textureSize.x * scale, textureSize.y * scale); - ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (availableSize.x * 0.5f) - (renderSize.y * 0.5f), + ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPosX() + (availableSize.x * 0.5f) - (renderSize.x * 0.5f), ImGui::GetCursorPosY() + (availableSize.y * 0.5f) - (renderSize.y * 0.5f))); ImGui::Image(character.render.id, renderSize); diff --git a/src/state/settings_menu.cpp b/src/state/settings_menu.cpp index 036097d..0214f7d 100644 --- a/src/state/settings_menu.cpp +++ b/src/state/settings_menu.cpp @@ -10,47 +10,61 @@ using namespace game::util; using namespace game::util::imgui; +using namespace game::resource::xml; namespace game::state { - void SettingsMenu::update(Resources& resources, Mode mode) + void SettingsMenu::update(Resources& resources, Mode mode, const Strings* strings) { auto& settings = resources.settings; auto& measurementSystem = settings.measurementSystem; auto& volume = settings.volume; auto& color = settings.color; + auto string_get = [&](Strings::Type type, const char* fallback) -> const char* + { + return strings ? strings->get(type).c_str() : fallback; + }; isJustColorSet = false; - ImGui::SeparatorText("Measurement System"); - WIDGET_FX(ImGui::RadioButton("Metric", (int*)&measurementSystem, measurement::METRIC)); - ImGui::SetItemTooltip("%s", "Use kilograms (kg)."); + ImGui::SeparatorText(string_get(Strings::SettingsMeasurementSystem, "Measurement System")); + WIDGET_FX(ImGui::RadioButton(string_get(Strings::SettingsMetric, "Metric"), + (int*)&measurementSystem, measurement::METRIC)); + ImGui::SetItemTooltip("%s", string_get(Strings::SettingsMetricTooltip, "Use kilograms (kg).")); ImGui::SameLine(); - WIDGET_FX(ImGui::RadioButton("Imperial", (int*)&measurementSystem, measurement::IMPERIAL)); - ImGui::SetItemTooltip("%s", "Use pounds (lbs)."); + WIDGET_FX(ImGui::RadioButton(string_get(Strings::SettingsImperial, "Imperial"), + (int*)&measurementSystem, measurement::IMPERIAL)); + ImGui::SetItemTooltip("%s", string_get(Strings::SettingsImperialTooltip, "Use pounds (lbs).")); - ImGui::SeparatorText("Sound"); - if (WIDGET_FX(ImGui::SliderInt("Volume", &volume, 0, 100, "%d%%"))) + ImGui::SeparatorText(string_get(Strings::SettingsSound, "Sound")); + if (WIDGET_FX( + ImGui::SliderInt(string_get(Strings::SettingsVolume, "Volume"), &volume, 0, 100, "%d%%"))) resources.volume_set(math::to_unit((float)volume)); - ImGui::SetItemTooltip("%s", "Adjust master volume."); + ImGui::SetItemTooltip("%s", string_get(Strings::SettingsVolumeTooltip, "Adjust master volume.")); - ImGui::SeparatorText("Appearance"); + ImGui::SeparatorText(string_get(Strings::SettingsAppearance, "Appearance")); - if (WIDGET_FX(ImGui::Checkbox("Use Character Color", &settings.isUseCharacterColor))) isJustColorSet = true; - ImGui::SetItemTooltip("When playing, the UI will use the character's preset UI color."); + if (WIDGET_FX(ImGui::Checkbox(string_get(Strings::SettingsUseCharacterColor, + "Use Character Color"), + &settings.isUseCharacterColor))) + isJustColorSet = true; + ImGui::SetItemTooltip("%s", string_get(Strings::SettingsUseCharacterColorTooltip, + "When playing, the UI will use the character's preset UI color.")); ImGui::SameLine(); ImGui::BeginDisabled(settings.isUseCharacterColor); if (WIDGET_FX( - ImGui::ColorEdit3("Color", value_ptr(color), ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoTooltip))) + ImGui::ColorEdit3(string_get(Strings::SettingsColor, "Color"), value_ptr(color), + ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoTooltip))) { style::color_set(color); isJustColorSet = true; } - ImGui::SetItemTooltip("%s", "Change the UI color."); + ImGui::SetItemTooltip("%s", string_get(Strings::SettingsColorTooltip, "Change the UI color.")); ImGui::EndDisabled(); ImGui::Separator(); - if (WIDGET_FX(ImGui::Button("Reset to Default", ImVec2(-FLT_MIN, 0)))) + if (WIDGET_FX(ImGui::Button(string_get(Strings::SettingsResetButton, "Reset to Default"), + ImVec2(-FLT_MIN, 0)))) { settings = resource::xml::Settings(); style::color_set(settings.color); @@ -60,11 +74,19 @@ namespace game::state { ImGui::Separator(); - if (WIDGET_FX(ImGui::Button("Save", ImVec2(-FLT_MIN, 0)))) isSave = true; - ImGui::SetItemTooltip("%s", "Save the game.\n(Note: the game autosaves frequently.)"); + if (WIDGET_FX( + ImGui::Button(string_get(Strings::SettingsSaveButton, "Save"), ImVec2(-FLT_MIN, 0)))) + isSave = true; + ImGui::SetItemTooltip( + "%s", string_get(Strings::SettingsSaveTooltip, + "Save the game.\n(Note: the game autosaves frequently.)")); - if (WIDGET_FX(ImGui::Button("Return to Characters", ImVec2(-FLT_MIN, 0)))) isGoToSelect = true; - ImGui::SetItemTooltip("%s", "Go back to the character selection screen.\nProgress will be saved."); + if (WIDGET_FX(ImGui::Button( + string_get(Strings::SettingsReturnToCharactersButton, "Return to Characters"), + ImVec2(-FLT_MIN, 0)))) + isGoToSelect = true; + ImGui::SetItemTooltip("%s", string_get(Strings::SettingsReturnToCharactersTooltip, + "Go back to the character selection screen.\nProgress will be saved.")); } } } diff --git a/src/state/settings_menu.hpp b/src/state/settings_menu.hpp index ca7b708..2afa1b6 100644 --- a/src/state/settings_menu.hpp +++ b/src/state/settings_menu.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../resource/xml/strings.hpp" #include "../resources.hpp" namespace game::state @@ -17,6 +18,6 @@ namespace game::state bool isSave{}; bool isJustColorSet{}; - void update(Resources&, Mode = SELECT); + void update(Resources&, Mode = SELECT, const resource::xml::Strings* = nullptr); }; } diff --git a/src/util/imgui/style.cpp b/src/util/imgui/style.cpp index 2a90f41..c4a4f97 100644 --- a/src/util/imgui/style.cpp +++ b/src/util/imgui/style.cpp @@ -18,16 +18,20 @@ namespace game::util::imgui::style static constexpr auto COLOR_BG_ALPHA = 0.90f; static constexpr auto COLOR_ACTIVE_MULTIPLIER = 1.30f; static constexpr auto COLOR_HOVERED_MULTIPLIER = 1.60f; + static constexpr auto COLOR_ACTIVE_HOVERED_MIN = 0.1f; + static constexpr auto COLOR_ACTIVE_HOVERED_MAX = 0.9f; static constexpr auto COLOR_ACCENT_MULTIPLIER = 2.0f; static constexpr auto COLOR_ACCENT_ACTIVE_MULTIPLIER = 2.25f; auto& colors = ImGui::GetStyle().Colors; + auto active_hovered_clamp = [](glm::vec3 color) + { return glm::clamp(color, glm::vec3(COLOR_ACTIVE_HOVERED_MIN), glm::vec3(COLOR_ACTIVE_HOVERED_MAX)); }; auto colorNew = to_imvec4(glm::vec4(color, 1.0f)); auto colorBg = to_imvec4(glm::vec4(color * COLOR_BG_MULTIPLIER, COLOR_BG_ALPHA)); auto colorChildBg = to_imvec4(glm::vec4(color * COLOR_BG_MULTIPLIER, 0.0f)); - auto colorActive = to_imvec4(glm::vec4(color * COLOR_ACTIVE_MULTIPLIER, 1.0f)); - auto colorHovered = to_imvec4(glm::vec4(color * COLOR_HOVERED_MULTIPLIER, 1.0f)); + auto colorActive = to_imvec4(glm::vec4(active_hovered_clamp(color * COLOR_ACTIVE_MULTIPLIER), 1.0f)); + auto colorHovered = to_imvec4(glm::vec4(active_hovered_clamp(color * COLOR_HOVERED_MULTIPLIER), 1.0f)); auto colorAccent = to_imvec4(glm::vec4(color * COLOR_ACCENT_MULTIPLIER, 1.0f)); auto colorAccentActive = to_imvec4(glm::vec4(color * COLOR_ACCENT_ACTIVE_MULTIPLIER, 1.0f)); diff --git a/src/util/preferences.cpp b/src/util/preferences.cpp index a135b83..68d40d0 100644 --- a/src/util/preferences.cpp +++ b/src/util/preferences.cpp @@ -7,11 +7,11 @@ namespace game::util::preferences std::filesystem::path path() { #ifdef __EMSCRIPTEN__ - static constexpr auto filePath = "/snivy"; + static constexpr auto filePath = "/shweets-sim"; std::filesystem::create_directories(filePath); return filePath; #else - auto sdlPath = SDL_GetPrefPath(nullptr, "snivy"); + auto sdlPath = SDL_GetPrefPath(nullptr, "shweets-sim"); if (!sdlPath) return {}; auto filePath = std::filesystem::path(sdlPath); std::filesystem::create_directories(filePath); diff --git a/src/util/web_filesystem.cpp b/src/util/web_filesystem.cpp index ead4fe1..28355ce 100644 --- a/src/util/web_filesystem.cpp +++ b/src/util/web_filesystem.cpp @@ -12,12 +12,12 @@ namespace game::util::web_filesystem Module.filesystemReady = 0; try { - FS.mkdir('/snivy'); + FS.mkdir('/shweets-sim'); } catch (e) { } - FS.mount(IDBFS, {}, '/snivy'); + FS.mount(IDBFS, {}, '/shweets-sim'); FS.syncfs( true, function(err) { if (err) console.error('IDBFS init sync failed', err); @@ -50,4 +50,4 @@ namespace game::util::web_filesystem idbfs_flush_async(); #endif } -} \ No newline at end of file +} diff --git a/web/index.html b/web/index.html index 7f16d22..06f5041 100644 --- a/web/index.html +++ b/web/index.html @@ -3,7 +3,7 @@ - snivy + shweets-sim