Compare commits

...

58 Commits

Author SHA1 Message Date
fb6f902f28 Inventory updates, API updates, lots of file renaming 2026-03-17 04:05:04 -04:00
b060784bb7 settings fix 2026-03-05 13:43:42 -05:00
9a2e03b146 cheat fix 2026-03-04 13:11:39 -05:00
b81296a4f2 new game bug fix 2026-03-04 13:06:35 -05:00
554d6198fd erm
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-02 20:27:40 -05:00
70b5277f87 one more fix for windows
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-02 20:19:49 -05:00
3375f56492 fix for empty sounds/animation collections
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-02 20:17:04 -05:00
03ee76e0a5 ffffffffffff
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-02 15:56:45 -05:00
a7f11f8842 nvm nvm
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-02 15:54:25 -05:00
3cec6a5541 helpful windows zip
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-02 15:50:51 -05:00
1f0a1d4f47 forgot one line of code award 🏅
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-02 15:38:57 -05:00
2d230ecd2e line height
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-02 14:35:10 -05:00
2a58c3b24b UPGRADE! + cheats
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-02 14:02:14 -05:00
475fb5a847 update modding
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 19:48:13 -05:00
06d2cbdc12 animation fixes + modding WIP
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 19:07:01 -05:00
6ed9a15177 Merge branch 'master' of https://github.com/ShweetsStuff/snivy
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 14:00:53 -05:00
Shweet
a9121b58a0 Merge pull request #6 from sertimus/missingdeclarations
Add missing standard library header directives
2026-03-01 10:59:27 -08:00
Sertimus
83ba5699ac Added some missing standard library header delcarations! 2026-03-01 13:28:07 -05:00
841ff371da fixes to stage up
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 12:17:09 -05:00
0b70bab618 update le readme :^)
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 03:55:40 -05:00
94db77e8da update screenies + web build assist
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 03:50:29 -05:00
154bccb3d5 The Mega Snivy Update (for realsies)
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 03:31:05 -05:00
0f855f7125 cursor issue
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 03:28:23 -05:00
2aaf6dcf75 name
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 03:23:29 -05:00
f594ba3889 Just last little bits of polish
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 03:19:49 -05:00
45ee9a7d11 rc
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 01:18:38 -05:00
b3c097be22 vscode tasks, build settings, etc.
Some checks failed
Build / Build Game (push) Has been cancelled
2026-03-01 01:09:02 -05:00
68d5301735 log error?
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 23:14:33 -05:00
d016768ca9 update colors
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 23:01:03 -05:00
f11436abaa no terminal!
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:57:32 -05:00
9dc34c72d4 lets try this
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:45:35 -05:00
ac41e4f31d erm?
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:37:05 -05:00
00b3a146d5 erm
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:33:25 -05:00
0d9d8ff96f let's try this?
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:30:53 -05:00
bc8fe78fce wowie
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:24:06 -05:00
3817f1cc39 again
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:19:24 -05:00
acb1505308 moar...
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:16:26 -05:00
e2a2d2c464 moar fixes
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:14:32 -05:00
cf8864b90b try this!
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:11:37 -05:00
04765ad058 fixing windows warnings/errors
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:09:09 -05:00
d749508c1c win32 update
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 22:05:05 -05:00
c116b5c075 add windows icon
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 21:54:13 -05:00
17f3348e94 The Mega Snivy Update
Some checks failed
Build / Build Game (push) Has been cancelled
2026-02-28 21:48:00 -05:00
8b2edd1359 Just so I don't lose current progress on 2.0...
Some checks failed
Build / Build Game (push) Has been cancelled
2026-01-10 03:07:06 -05:00
Shweet
1f1ac0db4d Merge pull request #4 from MarkSuckerberg/build-because-why-not
Some checks failed
Build / Build Game (push) Has been cancelled
Build CI
2025-12-30 23:15:10 -08:00
Mark Suckerberg
c2f0188174 Upload the artifact because sure 2025-12-30 19:22:46 -06:00
Shweet
b76604bacf Merge pull request #3 from MarkSuckerberg/play-feedback
Game Bar Highlight
2025-12-30 16:48:40 -08:00
Shweet
85663e3c9c Merge pull request #2 from MarkSuckerberg/inventory-adjustment
Inventory Display Tweak
2025-12-30 16:47:51 -08:00
Shweet
a81a549fa1 Merge pull request #1 from MarkSuckerberg/volume
Volume setting
2025-12-30 16:47:40 -08:00
Mark Suckerberg
1745899487 Just removes the windows build 2025-12-30 18:44:30 -06:00
Mark Suckerberg
d028224fd0 Ease the outline colour when clicked 2025-12-30 18:15:00 -06:00
Mark Suckerberg
0f4ba92de5 Adjusts to also hide impossible item silhouettes 2025-12-30 17:53:57 -06:00
Mark Suckerberg
2d2faebd10 tweaks inventory screen display 2025-12-30 17:44:12 -06:00
Mark Suckerberg
aa70543411 Tiny adjustment to make the play bar highlight on mouse hover 2025-12-30 15:20:00 -06:00
Mark Suckerberg
228c87d0ee forgot the flag I meant to add + fixes msbuild 2025-12-30 14:47:52 -06:00
Mark Suckerberg
20bc2837e6 ah well 2025-12-30 14:37:26 -06:00
Mark Suckerberg
1b67f1c14b Add a build action because there's no reason not to at least test that 2025-12-30 14:28:51 -06:00
Mark Suckerberg
c30a9e3520 Volume setting 2025-12-30 13:15:50 -06:00
186 changed files with 10594 additions and 13335 deletions

View File

@@ -1,3 +1,3 @@
CompileFlags:
CompilationDatabase: build
Add: [-std=c++20]
CompilationDatabase: out/build/linux-debug
Add: [-std=gnu++23]

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@ build/
build-web/
resources/
release/
out/
external/

6
.gitmodules vendored
View File

@@ -13,6 +13,6 @@
[submodule "external/tinyxml2"]
path = external/tinyxml2
url = https://github.com/leethomason/tinyxml2
[submodule "external/libanm2"]
path = external/libanm2
url = https://github.com/shweetsstuff/libanm2
[submodule "external/physfs"]
path = external/physfs
url = https://github.com/icculus/physfs

81
.vscode/launch.json vendored
View File

@@ -1,28 +1,61 @@
{
"version": "0.2.0",
"configurations": [
"version": "0.2.0",
"configurations": [
{
"name": "Debug (CMake Debug preset)",
"type": "cppdbg",
"request": "launch",
"preLaunchTask": "cmake: build debug",
"program": "${workspaceFolder}/out/build/linux-debug/bin/Debug/snivy",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/out/build/linux-debug/bin/Debug",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"setupCommands": [
{
"name": "Debug",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/snivy",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}/build",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing"
},
{
"description": "Set disassembly flavor to Intel",
"text": "-gdb-set disassembly-flavor intel"
}
]
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing"
},
]
{
"description": "Set disassembly flavor to Intel",
"text": "-gdb-set disassembly-flavor intel"
}
],
"windows": {
"type": "cppvsdbg",
"program": "${workspaceFolder}/out/build/x64-Debug/bin/Debug/snivy.exe",
"cwd": "${workspaceFolder}/out/build/x64-Debug/bin/Debug"
}
},
{
"name": "Run (CMake Release preset)",
"type": "cppdbg",
"request": "launch",
"noDebug": true,
"preLaunchTask": "cmake: build release",
"program": "${workspaceFolder}/out/build/linux-release/bin/Release/snivy",
"args": [],
"cwd": "${workspaceFolder}/out/build/linux-release/bin/Release",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "/usr/bin/gdb",
"windows": {
"type": "cppvsdbg",
"program": "${workspaceFolder}/out/build/x64-Release/bin/Release/snivy.exe",
"cwd": "${workspaceFolder}/out/build/x64-Release/bin/Release"
}
},
{
"name": "Web (Emscripten + Chromium)",
"type": "node-terminal",
"request": "launch",
"preLaunchTask": "web: run",
"command": "echo \"Web task complete. If Chromium did not open, run task: web: open chromium\"",
"cwd": "${workspaceFolder}"
}
]
}

205
.vscode/tasks.json vendored
View File

@@ -1,44 +1,165 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"type": "shell",
"command": "cmake --build build",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$gcc"
]
},
{
"label": "start-wasm-devserver",
"type": "shell",
"command": "${workspaceFolder}/scripts/start_wasm_dev.sh",
"presentation": {
"reveal": "silent",
"panel": "dedicated"
},
"problemMatcher": []
},
{
"label": "stop-wasm-devserver",
"type": "shell",
"command": "${workspaceFolder}/scripts/stop_wasm_dev.sh",
"problemMatcher": []
},
{
"type": "cmake",
"label": "CMake: build",
"command": "build",
"targets": [
"[N/A - Select Kit]"
],
"group": "build",
"problemMatcher": [],
"detail": "CMake template build task"
}
]
"version": "2.0.0",
"tasks": [
{
"label": "cmake: configure debug",
"type": "shell",
"linux": {
"command": "cmake --preset linux-debug"
},
"windows": {
"command": "cmake --preset x64-Debug"
},
"problemMatcher": []
},
{
"label": "cmake: build debug",
"type": "shell",
"linux": {
"command": "cmake --build --preset linux-debug"
},
"windows": {
"command": "cmake --build --preset x64-Debug"
},
"dependsOn": "cmake: configure debug",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$gcc"
]
},
{
"label": "cmake: configure release",
"type": "shell",
"linux": {
"command": "cmake --preset linux-release"
},
"windows": {
"command": "cmake --preset x64-Release"
},
"problemMatcher": []
},
{
"label": "cmake: build release",
"type": "shell",
"linux": {
"command": "cmake --build --preset linux-release"
},
"windows": {
"command": "cmake --build --preset x64-Release"
},
"dependsOn": "cmake: configure release",
"problemMatcher": [
"$gcc"
]
},
{
"label": "linux: run debug",
"type": "shell",
"linux": {
"command": "${workspaceFolder}/out/build/linux-debug/bin/Debug/snivy"
},
"options": {
"cwd": "${workspaceFolder}/out/build/linux-debug/bin/Debug"
},
"windows": {
"command": "echo \"linux: run debug is Linux-only\" && exit 1"
},
"dependsOn": "cmake: build debug",
"problemMatcher": []
},
{
"label": "linux: run release",
"type": "shell",
"linux": {
"command": "${workspaceFolder}/out/build/linux-release/bin/Release/snivy"
},
"options": {
"cwd": "${workspaceFolder}/out/build/linux-release/bin/Release"
},
"windows": {
"command": "echo \"linux: run release is Linux-only\" && exit 1"
},
"dependsOn": "cmake: build release",
"problemMatcher": []
},
{
"label": "web: configure",
"type": "shell",
"linux": {
"command": "sh -lc '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'"
},
"windows": {
"command": "echo \"web tasks are configured for Linux (emsdk + python + chromium).\" && exit 1"
},
"problemMatcher": []
},
{
"label": "web: build",
"type": "shell",
"linux": {
"command": "sh -lc '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; cmake --build out/build/web -j$(nproc)'"
},
"windows": {
"command": "echo \"web tasks are configured for Linux (emsdk + python + chromium).\" && exit 1"
},
"dependsOn": "web: configure",
"problemMatcher": []
},
{
"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; }'"
},
"windows": {
"command": "echo \"web tasks are configured for Linux (emsdk + python + chromium).\" && exit 1"
},
"problemMatcher": []
},
{
"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'"
},
"windows": {
"command": "echo \"web tasks are configured for Linux (emsdk + python + chromium).\" && exit 1"
},
"problemMatcher": []
},
{
"label": "web: open chromium",
"type": "shell",
"linux": {
"command": "sh -lc 'URL=http://127.0.0.1:8000/bin/Release/index.html; (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) & sleep 0.15; echo \"Requested browser open: $URL\"'"
},
"windows": {
"command": "start http://127.0.0.1:8000/index.html"
},
"problemMatcher": []
},
{
"label": "web: prepare",
"dependsOrder": "sequence",
"dependsOn": [
"web: build",
"web: serve"
],
"problemMatcher": []
},
{
"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\"'"
},
"windows": {
"command": "echo \"web tasks are configured for Linux (emsdk + python + chromium).\" && exit 1"
},
"problemMatcher": []
}
]
}

View File

@@ -6,11 +6,30 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(BUILD_SHARED_LIBS OFF)
if(MSVC)
set(CMAKE_SUPPRESS_REGENERATION ON)
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD OFF)
endif()
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
set(CMAKE_C_FLAGS_DEBUG "-O0 -g" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g" CACHE STRING "" FORCE)
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE)
if(NOT CMAKE_CONFIGURATION_TYPES)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
endif()
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS Debug Release)
endif()
if(MSVC)
set(CMAKE_C_FLAGS_DEBUG "/Od /Zi" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_DEBUG "/W4 /permissive- /Od /Zi" CACHE STRING "" FORCE)
set(CMAKE_C_FLAGS_RELEASE "/O2 /DNDEBUG" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_RELEASE "/W4 /permissive- /O2 /DNDEBUG" CACHE STRING "" FORCE)
else()
set(CMAKE_C_FLAGS_DEBUG "-O0 -g" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_DEBUG "-Wall -Wpedantic -O0 -g" CACHE STRING "" FORCE)
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_RELEASE "-Wall -Wpedantic -O3 -DNDEBUG" CACHE STRING "" FORCE)
endif()
endif()
set(SDL_STATIC ON CACHE BOOL "" FORCE)
@@ -56,12 +75,25 @@ set(SDLMIXER_TEST OFF CACHE BOOL "" FORCE)
set(SDLMIXER_INSTALL OFF CACHE BOOL "" FORCE)
add_subdirectory(external/SDL_mixer EXCLUDE_FROM_ALL)
set(PHYSFS_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(PHYSFS_BUILD_STATIC ON CACHE BOOL "" FORCE)
set(PHYSFS_BUILD_TEST OFF CACHE BOOL "" FORCE)
set(PHYSFS_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(PHYSFS_BUILD_WX_TEST OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_ZIP ON CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_7Z OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_GRP OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_HOG OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_PAK OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_WAD OFF CACHE BOOL "" FORCE)
set(PHYSFS_ARCHIVE_DIR ON CACHE BOOL "" FORCE)
set(PHYSFS_DISABLE_INSTALL ON CACHE BOOL "" FORCE)
add_subdirectory(external/physfs)
set(IMGUI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/imgui)
set(TINYXML2_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/tinyxml2)
set(GLM_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/glm)
set(PHYSFS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/external/physfs)
set(IMGUI_SOURCES
${IMGUI_DIR}/imgui.cpp
@@ -75,12 +107,19 @@ set(IMGUI_SOURCES
set (TINYXML2_SOURCES ${TINYXML2_DIR}/tinyxml2.cpp)
file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
include/*.cpp
src/*.cpp
src/resource/*.cpp
src/window/*.cpp
src/util/*.cpp
src/util/*.h
include/*.cpp
src/*.cpp
src/render/*.cpp
src/resource/*.cpp
src/resource/xml/*.cpp
src/state/*.cpp
src/state/play/*.cpp
src/state/play/arcade/*.cpp
src/state/select/*.cpp
src/entity/*.cpp
src/window/*.cpp
src/util/*.cpp
src/util/imgui/*.cpp
)
add_executable(${PROJECT_NAME}
@@ -90,10 +129,29 @@ add_executable(${PROJECT_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/include/glad/glad.cpp
)
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}/Icon.ico"
)
endif()
target_compile_definitions(${PROJECT_NAME} PRIVATE
"$<$<CONFIG:Debug>:DEBUG=1>"
"$<$<NOT:$<CONFIG:Debug>>:DEBUG=0>"
)
set_target_properties(${PROJECT_NAME} PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin/Debug"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin/Release"
)
target_include_directories(${PROJECT_NAME} PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/external/SDL/include
${CMAKE_CURRENT_SOURCE_DIR}/external/SDL_mixer/include
${CMAKE_CURRENT_SOURCE_DIR}/external # tinyxml2 headers are included as <tinyxml2/tinyxml2.h>
${CMAKE_CURRENT_SOURCE_DIR}/external
${IMGUI_DIR}
${IMGUI_DIR}/backends
${GLM_DIR}
@@ -103,24 +161,72 @@ target_include_directories(${PROJECT_NAME} PRIVATE
include
)
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3-static SDL3_mixer::SDL3_mixer-static)
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3-static SDL3_mixer::SDL3_mixer-static physfs-static)
if (WIN32 AND TARGET SDL3::SDL3main)
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3main)
endif()
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
if(MSVC)
target_compile_options(${PROJECT_NAME} PRIVATE
"$<$<CONFIG:Release>:/O2>"
"$<$<CONFIG:Release>:/DNDEBUG>"
)
if(WIN32)
target_link_options(${PROJECT_NAME} PRIVATE
"/SUBSYSTEM:WINDOWS"
"/ENTRY:mainCRTStartup"
)
endif()
target_link_options(${PROJECT_NAME} PRIVATE
"$<$<CONFIG:Release>:/DEBUG:NONE>"
"$<$<CONFIG:Release>:/INCREMENTAL:NO>"
"$<$<CONFIG:Release>:/OPT:REF>"
"$<$<CONFIG:Release>:/OPT:ICF>"
)
else()
target_compile_options(${PROJECT_NAME} PRIVATE
"$<$<CONFIG:Release>:-O3>"
"$<$<CONFIG:Release>:-DNDEBUG>"
)
if(WIN32)
target_link_options(${PROJECT_NAME} PRIVATE
"-mwindows"
)
endif()
target_link_options(${PROJECT_NAME} PRIVATE
"$<$<CONFIG:Release>:-s>"
)
endif()
endif()
set(PROJECT_RESOURCES_DIR "${CMAKE_CURRENT_SOURCE_DIR}/resources")
set(PROJECT_RESOURCES_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/resources")
set(PROJECT_RESOURCES_BINARY_DIR "${CMAKE_BINARY_DIR}/bin/$<CONFIG>/resources")
if(EXISTS "${PROJECT_RESOURCES_DIR}")
file(GLOB_RECURSE PROJECT_RESOURCE_FILES CONFIGURE_DEPENDS
"${PROJECT_RESOURCES_DIR}/*")
add_custom_target(copy_resources ALL
COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_RESOURCES_DIR}" "${PROJECT_RESOURCES_BINARY_DIR}"
DEPENDS ${PROJECT_RESOURCE_FILES}
COMMENT "Copying resources directory")
add_dependencies(${PROJECT_NAME} copy_resources)
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()
set(HAS_PROJECT_RESOURCES TRUE)
else()
set(HAS_PROJECT_RESOURCES FALSE)
endif()
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION .)
if(HAS_PROJECT_RESOURCES)
install(DIRECTORY "${PROJECT_RESOURCES_DIR}/" DESTINATION resources)
endif()
if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
set(EMSCRIPTEN_SHELL_FILE "${CMAKE_CURRENT_SOURCE_DIR}/web/index.html")
target_link_options(${PROJECT_NAME} PRIVATE
"-sMIN_WEBGL_VERSION=2"
"-sMAX_WEBGL_VERSION=2"
@@ -128,21 +234,43 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
"-sUSE_OGG=1"
"-sUSE_VORBIS=1"
"-sALLOW_MEMORY_GROWTH=1"
"-sFORCE_FILESYSTEM=1"
"-sASYNCIFY"
"-lidbfs.js"
"--shell-file"
"${EMSCRIPTEN_SHELL_FILE}"
)
if(HAS_PROJECT_RESOURCES)
target_link_options(${PROJECT_NAME} PRIVATE
"--preload-file"
"${PROJECT_RESOURCES_BINARY_DIR}@resources"
"${PROJECT_RESOURCES_DIR}@resources"
)
endif()
set_target_properties(${PROJECT_NAME} PROPERTIES
OUTPUT_NAME "index"
SUFFIX ".js")
SUFFIX ".html")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND}
-DBIN_DIR="$<TARGET_FILE_DIR:${PROJECT_NAME}>"
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/create_index_zip.cmake"
COMMENT "Creating snivy-web.zip from Emscripten output")
else()
find_package(OpenGL REQUIRED COMPONENTS OpenGL)
target_link_libraries(${PROJECT_NAME} PRIVATE OpenGL::GL)
endif()
if(WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND}
-DBIN_ROOT="${CMAKE_BINARY_DIR}/bin"
-DTARGET_DIR="$<TARGET_FILE_DIR:${PROJECT_NAME}>"
-DEXE_FILE="$<TARGET_FILE_NAME:${PROJECT_NAME}>"
-DPACKAGE_NAME="snivy-win32"
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/create_windows_zip.cmake"
COMMENT "Creating snivy-win32 package")
endif()
message(STATUS "System: ${CMAKE_SYSTEM_NAME}")
message(STATUS "Project: ${PROJECT_NAME}")
message(STATUS "Compiler: ${CMAKE_CXX_COMPILER}")

94
CMakePresets.json Normal file
View File

@@ -0,0 +1,94 @@
{
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 27,
"patch": 0
},
"configurePresets": [
{
"name": "linux-debug",
"displayName": "Linux Debug",
"description": "Ninja Debug (Linux, native)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/linux-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/linux-debug"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "linux-release",
"displayName": "Linux Release",
"description": "Ninja Release (Linux, native)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/out/build/linux-release",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/linux-release"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Linux"
}
},
{
"name": "win32-debug",
"displayName": "Win32 Debug",
"description": "Visual Studio 2022 x64 Debug",
"generator": "Visual Studio 17 2022",
"architecture": "x64",
"binaryDir": "${sourceDir}/out/build/win32-debug",
"cacheVariables": {
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/win32-debug"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "win32-release",
"displayName": "Win32 Release",
"description": "Visual Studio 2022 x64 Release",
"generator": "Visual Studio 17 2022",
"architecture": "x64",
"binaryDir": "${sourceDir}/out/build/win32-release",
"cacheVariables": {
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install/win32-release"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
}
],
"buildPresets": [
{
"name": "linux-debug",
"configurePreset": "linux-debug"
},
{
"name": "linux-release",
"configurePreset": "linux-release"
},
{
"name": "win32-debug",
"configurePreset": "win32-debug",
"configuration": "Debug"
},
{
"name": "win32-release",
"configurePreset": "win32-release",
"configuration": "Release"
}
]
}

28
CMakeSettings.json Normal file
View File

@@ -0,0 +1,28 @@
{
"configurations": [
{
"name": "win32-debug",
"generator": "Ninja",
"configurationType": "Debug",
"inheritEnvironments": [
"msvc_x64_x64"
],
"buildRoot": "${projectDir}\\out\\build\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": ""
},
{
"name": "win32-release",
"generator": "Ninja",
"configurationType": "Release",
"inheritEnvironments": [
"msvc_x64_x64"
],
"buildRoot": "${projectDir}\\out\\build\\${name}",
"cmakeCommandArgs": "",
"buildCommandArgs": "",
"ctestCommandArgs": ""
}
]
}

BIN
Icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

602
MODDING.md Normal file
View File

@@ -0,0 +1,602 @@
# Modding Feed Snivy
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.
# 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.
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).
Anm2Ed has slightly more extensions to the base format; in particular, something called "Regions", which allows you to set spritesheet regions (areas in the spritesheet that can be repurposed) to quickly reference in animations; previously, one would have to manually create a perfectly spaced spritesheet and then manually set values; this is no longer the case, so using "Regions" is recommended. For the leanest files, in Anm2Ed, go to Settings -> Configure -> File -> Compatibility, and set it to "Anm2Ed Limited".
My process for making characters is that they're typically comprised of many "cells" (small graphics). I make these cells their own file, and then in the editor, I drag them onto "Spritesheets" and then right click -> merge them, with the "Make Spritesheet Regions" option enabled. Then, right click -> Pack on a spritesheet to optimize it all nice for one big spritesheet atlas with everything in one place. It won't produce a very comprehensible spritesheet, but it works just fine (not my problem). Make sure to save the spritesheets you're using once you're done. You can sort regions around by clicking, holding and moving them around.
If you'd like a quick primer on how to use either program, check either of [these](https://www.youtube.com/watch?v=qU_GMo2l7NY) [videos](https://www.youtube.com/watch?v=QiXVM6Gwzlw) out. Much of the information is interchangeable between the programs, though both are a little out of date on their current version.
For each "stage" of the character (graphical change), an animation equivalent will be expected. Stages are zero-indexed in this context; so the first stage would be 0. If you have an idle animation and want that for the first, starting stage, you'd need to name the animation "Idle0", for example, and then reference that in character.xml (more on that later). Don't manually input the number for the animation in any file, just put in the first part, "Idle"; the game will handle which stage-animation plays when.
Know that despite Anm2Ed supporting sounds for animations, don't expect these to work in context based on names; use the .xml sound attributes when applicable. You can add sounds for fluff, though.
# Resources
There's two folders inside resources; "characters" and "font".
"font" is just a font containing the font used in the selection screen. That's literally it. This technically isn't needed; Dear ImGui has its own font that will be used if this font is missing.
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:
- areas.xml
- character.xml
- cursor.xml
- dialogue.xml
- items.xml
- menu.xml
- play.xml
(dialogue.xml may or may not be optional; but in future updates I'll make sure of it, for dialogueless characters).
If you're making your own character, your best bet would probably be just to copy the Snivy character and edit it based on your needs, just as a helpful start.
# Character
## areas.xml
"Areas" refers to the backgrounds of the game. I'd planned for the game to dynamically switch areas based on stage (in case you want a character to become a blob that grows bigger than a city or whatever) but this isn't implemented. I'd just have one entry in here for the game's background.
### Areas
#### TextureRootPath (path)
Working folder/directory of textures the areas will use.
#### Area
#### Texture (path)
The file path of the area's texture (background).
#### Gravity (float)
Gravity the area has; applies to items' velocities per tick.
#### Friction (float)
Friction of the area; applies to items' velocities when grounded or hitting walls.
#### AirResistance (float)
Air resistance of the area; applies to items' velocities when airborne.
## character.xml
This is the main character file where much of the functionality is stored.
### Character
#### Name (string)
Name of the character; will appear in dialogue, stats, etc.
#### TextureRootPath (path)
Working folder/directory of where used textures will be contained within.
#### SoundRootPath (path)
Working folder/directory of where used sounds will be contained within.
#### Render (path)
Texture for the character's "render" (i.e., a typical full-body display), will show in the Select screen.
#### Portrait (path)
Texture for the character's "portrait" (i.e., a cropped profile view), will show in the Select screen.
#### Anm2 (path)
Character's "anm2" file (uses TextureRootPath); should have all the character's animations.
#### Description (string)
A general description of the character; will show in the Select screen.
#### Author (string)
The author of the character.
#### Weight (float, kilograms)
The character's starting weight, in kilograms.
#### Capacity (float, calories)
The character's starting capacity, in calories.
#### CapacityMin (float, calories)
The character's minimum capacity, in calories.
#### CapacityMax (float, calories)
The character's maximum capacity, in calories. Know that max capacity is determined by Capacity * CapacityMaxMultiplier; this determines the max of the "base" capacity.
#### CapacityMaxMultiplier (float)
Determines the effective max capacity; will be capacity times this number.
#### CapacityIfOverStuffedOnDigestBonus (float, percent)
When a character is over stuffed (i.e., over base capacity), the character will an additional capacity when digesting based on how overstuffed they are, based on how many calories over the base capacity.
#### CaloriesToKilogram (float)
Determines how many calories become a kilogram (1 cal -> X kg).
#### DigestionRate (float, percent/tick)
The base digestion rate, in percent per tick (60 ticks per second).
#### DigestionRateMin (float, percent/tick)
The minimum digestion rate for the character, in percent per tick.
#### DigestionRateMax (float, percent/tick)
The maximum digestion rate for the character, in percent per tick.
#### DigestionTimerMax (int, ticks)
When digesting, the digestion bar will count down, and then when it hits 0, the current calories will be digested. This determines how long this takes, in ticks.
#### EatSpeed (float)
A multiplier that speeds/slows down the eating animation, at base.
#### EatSpeedMin (float)
Determines the minimum eating speed multiplier.
#### EatSpeedMax (float)
Determines the maximum eating speed multiplier.
#### GurgleChance (float, percent)
Determines how often the character will gurgle (see the Gurgle sounds later) per tick.
#### GurgleCapacityMultiplier (float)
Per the character's capacity, multiplies the character's gurgle chance based on the percent of capacity filled (based on max capacity). Higher capacity = higher gurgle chance, using this number at maximum.
#### DialoguePoolID (string)
Determines the character's base dialogue options (for the "How are you feeling?" option in Chat). This is effectively "Stage 1"'s dialogue; each stage should have its own dialogue pool (see later). Also see dialogue.xml for how "Pools" work.
### AlternateSpritesheet
Determines the alternate spritesheet of the character, if applicable (in Pokemon terms, the "shiny").
#### Texture (path)
The alternate spritesheet texture; uses TextureRootPath.
#### Sound (path)
The sound that will play when the spritesheet is set to alternate (either on new game, or with an item that has IsToggleSpritesheet; see items.xml); uses SoundRootPath.
#### ID (int)
ID of spritesheet that the alternate spritesheet will replace in the character's .anm2.
#### ChanceOnNewGame (float, percent)
Chance of rolling for the alternate spritesheet on starting a new game, in percent.
### Stages
A "stage" represents each visual change of the character as the weight increases. By default, there's always one stage; adding more here will add additional stages.
#### Threshold (float, kilograms)
The weight threshold to reach this stage, in kilograms.
#### DialoguePoolID (string)
Determines the stage's dialogue options (for the "How are you feeling?" option in Chat). Also see dialogue.xml for how "Pools" work.
### Animations
The character's animations. Know that a lot of character animations are typically easily played/activated through Dialogue; this is just for animations that aren't reliant on that system. Know that _these animations should just be the base name_; all animations are expected to have a stage number after them, so don't include the number for these.
### Start
The animation name that will first play when starting a new game; used for like a character's introduction.
### Idle
The idle animation that the character will regularly return to.
### IdleFull
When over capacity, this idle animation will be used instead.
### StageUp
When going to a new stage after digestion and going over a stage's threshold, this animation will play.
#### Animation
The name of the animation to be used for each of these.
### Overrides
"Overrides" are the term I use for when some animations will be tweaked by the game engine for effect (blinking and talking being the ones used). There are two elements; "Talk" and "Blink" for this. Typically, how this is handled is that each animation with have an invisible blinking/talking layer (just the graphic that blinks), which will change when called for. Again, review the .anm2 format for what a "layer" is. All that's sourced is just the spritesheet crop in these cases; so don't worry about how the blink/talk layers are set up.
### Override
#### LayerSource (string)
The layer in the animation which will be sourced for the override.
#### LayerDestination (string)
The layer in the animation which the source will be applied to.
### EatAreas
An "eat area" is where the food should be dragged to. I'm pretty much expecting this to only be a mouth for most characters but hey maybe you want the character to also eat through their butt or something. Again, not something that's well-developed at the moment.
### EatArea
#### Null (string)
The null area in the animation where food will be checked for. Again, review the .anm2 format.
#### Animation (string)
The animation that will play when food is dragged to it.
#### Event (string)
The event that will be checked for, detrermining the time the food is actually eaten/chewed. Again, review the .anm2 format.
### ExpandAreas
The areas which will expand based on capacity percent; usually a stomach. This adds additional scale onto the layer of an animation. This also can scale a null as well.
ExpandAreas are presently hardcoded to scale with both capacity and weight at a 50/50 ratio; this could be changed in the future for custom ratios.
#### ExpandArea
#### Layer (string)
The layer of the animation that will expand.
#### Null (string)
The null of the animation that will expand.
#### ScaleAdd (float, percent)
The scale that will be added at maximum. Know that scale is a percent; a {100, 100} scale is the default and is the normal scale of an animation.
### InteractAreas
The areas on a character which can be interacted with; usually for belly rubs, kisses, etc.
### InteractArea
#### Type ("Rub", "Kiss", "Smack")
Three types are presently hard-coded in, but this is kind of hacky and custom support for different interactions may later be added. The cursor will need to be set from the "Tools" in order for each interact area to be activated.
#### Null (string)
The null in which the interact area can be triggered. Again, review the .anm2 format.
#### Animation (string)
The animation that will play when the interact area is activated (hovering and clicking).
#### AnimationFull (string)
The above, but when character is full.
#### AnimationCursorHover (string)
The animation the cursor will play when hovering over the interact area.
#### AnimationCursorActive (string)
The animation the cursor will play when holding down click over the interact area.
#### DialoguePoolID (string)
The dialogue pool which will be drawn from when activating an interact area (see dialogue.xml).
### Sound
An interact area can play multiple sounds when interacting; add additional Sound elements to achieve this. Don't worry about repeatedly-loaded sounds; the game will cache them beforehand for efficiency.
#### Path (path)
The path of the sound being used.
### Sounds
Sounds that play in some contexts.
### Digest
Sounds that will play when the character begins digesting (digestion bar full).
### Gurgle
Ambient sounds that will play randomly based on capacity; see GurgleChance and GurgleCapacityMultiplier above.
#### Sound (path)
The path of the sound for the specific sound type.
## cursor.xml
Determines the cursor appearance and behavior.
### TextureRootPath (path)
Working folder/directory of textures the cursor will use.
#### SoundRootPath (path)
Working folder/directory of sounds the cursor will use.
#### Anm2 (path)
The anm2 file the cursor will use (depends on TextureRootPath).
### Animations
### Idle
The cursor's default idle animation.
### Hover
The cursor's hover animation, when hovering over an item.
### Grab
The cursor's grab animation, when grabbing an item.
### Pan
The cursor's pan animation, when panning with middle mouse button.
### Zoom
The cursor's zoom animation, when zooming in/out with the mouse wheel.
### Return
The cursor's return animation, when holding right click to return an item to the inventory (or to dispose of it if chewed.)
#### Animation (string)
The animation the cursor will use in these contexts.
### Sounds
Know that multiple of these sounds can be defined for each context.
### Grab
The sound that will play when grabbing an item.
### Release
The sound that will play when releasing an item.
### Throw
The sound that will play when throwing an item (releasing when dragging quickly).
#### Sound (path)
The path of the sound for these respective contexts.
## dialogue.xml
The collection of character dialogue; likely the biggest file, depending on your needs.
### Dialogue
### Entries
### Entry
Each bit of dialogue is referred to as an "entry".
#### ID (string)
Name for the entry. Other entries will rely on this for dialogue chains.
#### Next (string)
The ID of the entry that will follow after this one. If no Next entry, the text will not continue. Make sure to connect these.
#### Text (string)
The actual dialogue content of the entry. Should support Unicode, provided the font contains the characters.
#### Animation (string)
A character animation that will play at the beginning of the dialogue. This may be expanded into the future to allow animations to play dynamically per dialogue index. Again, make sure it's just the base name for the animation, no stage numbers.
### Choice
Dialogue can be branching; simply added "Choice" elements into the Entry element.
#### Next (string)
The dialogue the choice will lead to.
#### Text (string)
The text of the choice; will appear as a series of buttons in the text window.
### Pools
A "pool" of dialogue refers to a collection of entries that can be randomly picked in some contexts.
### Pool
#### ID (string)
The name of the dialogue pool; can be referenced elsewhere.
### PoolEntry
### ID (string)
The name of the entry; to be added to the pool.
### Start
This dialogue will play upon a new game, once its animation has concluded.
#### Animation (string)
The name of the animation that will be played.
#### ID (string)
The name of the entry that will be played.
### End
The dialogue that will play upon completion of the game (character hitting max stage).
#### ID (string)
The name of the entry that will be played.
### Help
The dialogue that will be play when the player presses the "Help" button in "Chat".
#### ID (string)
The name of the entry that will be played.
### StageUp
Dialogue that will play after a character undergoes a stage up.
#### PoolID (string)
The name of the pool that entries will be drawn from.
### Random
The dialogue that will be play when the player presses the "Let's chat!" button in "Chat".
#### PoolID (string)
The name of the pool that entries will be drawn from.
### Feed
The dialogue that will be play when the user begins holding a food item.
#### PoolID (string)
The name of the pool that entries will be drawn from.
### FeedFull
The dialogue that will be play when the user begins holding a food item, when the character is over capacity.
#### PoolID (string)
The name of the pool that entries will be drawn from.
### Eat
The dialogue that will be play after the character finishes a food item.
#### PoolID (string)
The name of the pool that entries will be drawn from.
### EatFull
The dialogue that will be play after the character finishes a food item, when the character is over capacity.
#### PoolID (string)
The name of the pool that entries will be drawn from.
### Full
The dialogue that will be play when the character is completely full and will deny the user feeding them.
#### PoolID (string)
The name of the pool that entries will be drawn from.
### Throw
The dialogue that will be play when the user throws food.
#### PoolID (string)
The name of the pool that entries will be drawn from.
### LowCapacity
The dialogue that will be play when the character is presented a food item completely over their maximum capacity.
#### PoolID (string)
The name of the pool that entries will be drawn from.
### Digest
The dialogue that will be play when digesting is finished.
#### PoolID (string)
The name of the pool that entries will be drawn from.
### FoodTaken
The dialogue that will play when food is removed from the null area during a character's eating animation.
#### PoolID (string)
The name of the pool that entries will be drawn from.
### FoodTakenFull
The dialogue that will play when food is removed from the null area during a character's eating animation, when the character is over capacity.
#### PoolID (string)
The name of the pool that entries will be drawn from.
## items.xml
Has all items and their parameters, alongside additional behavior.
### ItemSchema
#### TextureRootPath (path)
Working folder/directory of where used textures will be contained within.
#### SoundRootPath (path)
Working folder/directory of where used sounds will be contained within.
#### BaseAnm2 (path)
The base anm2 file that items will use. Items can technically have their own .anm2 and animate accordingly, but I haven't tested this much.
### Animations
### Chew
When items are chewed, play this animation. Items can have chew counts and the animation will adapt accordingly, so be sure to have chew animations for chew counts; 1 chew = Chew1, 2 chew = Chew2, etc.
#### Animation (string)
The name of the animation. Only the base part ("Chew", usually)
### Sounds
The various item sounds. Multiple of the same element can be defined to randomly play from a selection.
### Bounce
The sound that will play when an item is bounced (hitting a floor/wall/ceiling with velocity).
#### Sound (path)
The path to the sound that will play, based on SoundRootPath.
### Dispose
The sound that will play when an item is disposed of (when an item has chewed and is right clicked).
#### Sound (path)
The path to the sound that will play, based on SoundRootPath.
### Summon
The sound that will play when an item is spawned (clicking on it in inventory).
#### Sound (path)
The path to the sound that will play, based on SoundRootPath.
### Return
The sound that will play when an item is returned to inventory (right clicking on it in world).
#### Sound (path)
The path to the sound that will play, based on SoundRootPath.
### Categories
The kinds of items present. Will appear in inventory tooltips.
### Category
#### Name (string)
The name of the category.
#### IsEdible (bool)
Determines if the category of item can be eaten by the character.
### Flavors
Item flavors (for food). Honestly have no meaning but flavor text at the moment but whatever.
### Flavor
#### Name (string)
Name of the flavor.
### Rarities
Different rarities of item. Shows in inventory and each can have their own drop chance.
### Rarity
#### Name (string)
Name of the rarity.
#### Chance (float, percent)
Base chance of finding an item, per winning a play of the "play" game. See play.xml for how this is modified.
#### Sound (path)
Sound that will play when an item is found in the "play" game.
#### IsHidden (bool)
If true, items with this rarity will not be previewed in the inventory, unlesss possessed. Assumed false if not present.
### Items
#### TextureRootPath (path)
Working folder/directory of where used item textures will be contained within.
#### ChewCount (int)
Base chew count for items; will be chewed this many times before being erased.
Chew count will determine how many calories/digestion bonus/etc. are given per bite (0 chew = all, 2 chew = divided by 3)
#### SpritesheetID (int)
Item textures will use this spritesheet ID on the base anm2.
#### QuantityMax (int)
How many items of a type can be possessed at once.
### Item
An individual item entry.
#### Name (string)
The name of the item.
#### Texture (path)
The texture the item uses; uses TextureRootPath.
#### Description (string)
Flavor text for the item.
#### Category (string)
The category the item uses; as defined in Categories.
#### Rarity (string)
The rarity the item uses; as defined in Rarities.
#### Anm2 (path; optional)
The custom anm2 the item uses, if needed.
#### Flavor (string; optional)
The flavor the item uses; as defined in Flavors.
#### Calories (float; optional)
The amount of calories in an item has (if food).
#### DigestionBonus (float, percent; optional)
The additional digestion rate in percent the item will give if eaten.
#### EatSpeedBonus (float; optional)
The additional eat speed multiplier the item will give if eaten.
#### Gravity (float; optional)
The item's gravity; will use the default gravity if not available.
#### ChewCount (int; optional)
An item's custom chew count.
#### UpgradeID (string; optional)
The name of another item that this item will be able to be upgraded to.
#### UpgradeCount (int; optional)
The amount of this item it will take to upgrade to the upgrade item specified in UpgradeID.
#### IsPlayReward (bool; optional)
The item will be given out when the reward is hit in play (see play.xml)
#### IsToggleSpritesheet (bool; optional)
When used in the inventory, will toggle the character's spritesheet (in Pokemon terms, toggling normal/shiny palettes).
## menu.xml
Determines menu and general UI appearance and behavior.
### MenuSchema
#### SoundRootPath (path)
Working folder/directory of where used sounds will be contained within.
#### FontRootPath (path)
Working folder/directory of where used fonts will be contained within.
#### Font (path)
The font the menu will use, based on FontRootPath.
#### Rounding (float)
The rounding of corners the UI will use.
### Sounds
Menu sounds.
### Open
Will play when opening a sliding menu panel.
#### Sound (path)
The sound that will play, based on SoundRootPath.
### Close
Will play when closing a sliding menu panel.
#### Sound (path)
The sound that will play, based on SoundRootPath.
### Hover
Will play when hovering over a widget in a menu.
#### Sound (path)
The sound that will play, based on SoundRootPath.
### Select
Will play when clicking on a widget in a menu.
#### Sound (path)
The sound that will play, based on SoundRootPath.
### CheatsActivated
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.
### Play
#### SoundRootPath (path)
Working folder/directory of where used sounds will be contained within.
#### RewardScore (int)
The play score where a rewarded item will be given (see items.xml)
#### RewardScoreBonus (float, percent)
Based on the player's score, will add additional bonus to being rewarded items.
#### RewardGradeBonus (float, percent)
Based on the player's grades for a round of play, will add additional bonus to being rewarded items.
#### SpeedMin (float, unit/tick)
The beginning/base speed of the bar in the minigame.
#### SpeedMax (float, unit/tick)
The maximum speed the bar in the minigame can achieve.
#### SpeedScoreBonus (float)
The additional speed of the bar based on the player's score.
#### RangeBase (float)
A "range" is each area in the minigame that can be hit. This is the base size of one. Per each grade, it's halved. Range size decreases as minigame progresses.
#### RangeMin (float)
The minimum size of a range; achievable at high scores.
#### RangeScoreBonus (float)
Really a negative. The range size will decrease this much per point of score.
#### EndTimerMax (int)
The period of time between plays of the minigame.
#### EndTimerFailureMax (int)
When a player fails (no ranges hit), the minigame will pause fo this amount of time, until restarting.
### Sounds
### Fall
The sound that plays when an item falls from being rewarded.
#### Sound (path)
The sound that will play, based on SoundRootPath.
### ScoreLoss
The sound that plays when the bar goes over the edge and loops, deducting a point.
#### Sound (path)
The sound that will play, based on SoundRootPath.
### HighScore
The sound that plays when the player achieves a high score. Only heard when a high score has been set during the play session.
#### Sound (path)
The sound that will play, based on SoundRootPath.
### HighScoreLoss
The sound that plays when the player has achieved a high score, and then fails.
#### Sound (path)
The sound that will play, based on SoundRootPath.
### RewardScore
The sound that plays when the player has hit the reward score.
#### Sound (path)
The sound that will play, based on SoundRootPath.
### Grades
A "grade" is a rank the player can get. This determines the count of ranges in the minigame; each range corresponds to a grade.
### Grade
#### Name (string)
The name of the grade; will appear when the player hits its respective range in the minigame.
#### NamePlural (string)
The plural name of the grade; will appear in Stats.
#### Value (int)
The reward value of the grade, in points.
#### Weight (float)
The "weight" of the grade; used to determine accuracy score in Stats.
#### IsFailure (bool)
If the player hits this grade's respective range, will count as failure.
#### DialoguePoolID (string; optional)
If the player hits this grade, the character will speak dialogue from this pool (see dialogue.xml)
#### Sound (path)
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.
## settings.xml
Stores general game settings and configuration; beyond invididual characters.
### MeasurementSystem ("Imperial", "Metric")
Determines measurement system (kg/lb).
### Volume (int, percent)
Master volume.
### ColorR, ColorG, ColorB (float, 0-1)
Red/Green/Blue components of menu color.
### WindowX, WindowY (int)
Window position.
### WindowW/H (int)
Window size.
## *.save
Saves are per-character; will be named "[blank].save", stored in "saves" folder, from the name of the character's archive.
### Save
#### IsPostgame (bool)
Determines if the game has been completed (character's max stage has been reached); if true, will enable cheats.
#### IsAlternateSpritesheet (bool)
Determines if the character's spritesheet is using the alternate version (in Pokemon, would be the "shiny" version
### Character
#### Weight (float)
Character's current weight, in kilograms.
#### Calories (float)
Character's current consumed calories.
#### Capacity (float)
Character's capacity, in calories. Remember, max capacity effectively is capacity * CapacityMaxMultiplier (from character.xml)
#### DigestionRate (float, percent)
Character's digestion rate in percent, per game tick (game ticks 60 times per second).
#### EatSpeed (float)
Eat speed multiplier for the character's Eat animation.
#### IsDigesting (bool)
Determines if character is currently digesting (when the bar is going down)
#### DigestionProgress (float, percent)
Character's digestion progress. Digestion max is always 100%.
#### DigestionTimer (int)
When digestion bar is going down, this is the remaining time to 0 (in ticks)
#### TotalCaloriesConsumed (float)
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
#### TotalPlays (int)
However many times the "play" game has been attempted (hitting the bar counts as one "play")
#### HighScore (int)
Highest score the player has achieved in the "play" game.
#### BestCombo (int)
Highest combo the player has achieved (how many successful hits the player has gotten in one session)
#### Grades
Play grades are the ratings the game gives based on where the player hit.
##### ID (int)
ID of grade being tracked (see play.xml)
##### Count (int)
How many times the grade has been hit.
### Inventory
Items.
#### ID (int)
ID of item being tracked (see items.xml)
#### Quantity (int)
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? :^) )

View File

@@ -4,12 +4,11 @@
This Is A Video Game Where You Feed The Snivy.
[Get Resources Here](https://shweetz.net/files/games/feed-snivy/resources.7z)
[Resources](https://shweetz.net/files/games/feed-snivy/resources.zip)
## Build
After cloning and enter the repository's directory, make sure to initialize the submodules:
```git submodule update --init --recursive```
### Windows
@@ -23,3 +22,8 @@ cd build
cmake ..
make
```
If using VSCode, several tasks are available to quickly run and build.
### Emscripten
Run the VSCode "web" task. Make sure you have the Emscripten SDK accessible at the EMSDK path variable.

View File

@@ -0,0 +1,55 @@
if(NOT DEFINED SRC_DIR OR NOT DEFINED DST_DIR)
message(FATAL_ERROR "SRC_DIR and DST_DIR 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.
file(COPY "${SRC_DIR}/" DESTINATION "${DST_DIR}"
PATTERN "characters/*" EXCLUDE)
# Copy only .zip archives from resources/characters.
file(MAKE_DIRECTORY "${DST_DIR}/characters")
file(GLOB CHARACTER_ZIPS "${CHARACTERS_DIR}/*.zip")
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$")
if(CHARACTER_FILES)
execute_process(
COMMAND "${CMAKE_COMMAND}" -E tar cf "snivy.zip" --format=zip ${CHARACTER_FILES}
WORKING_DIRECTORY "${CHARACTERS_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")
endif()
endif()
endif()
if(CHARACTER_ZIPS)
file(COPY ${CHARACTER_ZIPS} DESTINATION "${DST_DIR}/characters")
endif()

View File

@@ -0,0 +1,28 @@
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")
file(REMOVE "${ARCHIVE_PATH}")
file(GLOB INDEX_OUTPUTS "${BIN_DIR}/index.*")
set(FILES_TO_ZIP "")
foreach(FILE_PATH IN LISTS INDEX_OUTPUTS)
if(NOT FILE_PATH STREQUAL ARCHIVE_PATH)
get_filename_component(FILE_NAME "${FILE_PATH}" NAME)
list(APPEND FILES_TO_ZIP "${FILE_NAME}")
endif()
endforeach()
if(FILES_TO_ZIP)
execute_process(
COMMAND "${CMAKE_COMMAND}" -E tar cf "snivy-web.zip" --format=zip ${FILES_TO_ZIP}
WORKING_DIRECTORY "${BIN_DIR}"
RESULT_VARIABLE ZIP_RESULT
)
if(NOT ZIP_RESULT EQUAL 0)
message(FATAL_ERROR "Failed creating ${ARCHIVE_PATH}")
endif()
else()
message(WARNING "No index.* files found in ${BIN_DIR}; skipping snivy-web.zip creation")
endif()

View File

@@ -0,0 +1,47 @@
if(NOT DEFINED BIN_ROOT OR BIN_ROOT STREQUAL "")
message(FATAL_ERROR "BIN_ROOT is required")
endif()
if(NOT DEFINED TARGET_DIR OR TARGET_DIR STREQUAL "")
message(FATAL_ERROR "TARGET_DIR is required")
endif()
if(NOT DEFINED EXE_FILE OR EXE_FILE STREQUAL "")
message(FATAL_ERROR "EXE_FILE is required")
endif()
if(NOT DEFINED PACKAGE_NAME OR PACKAGE_NAME STREQUAL "")
set(PACKAGE_NAME "snivy-win32")
endif()
set(EXE_PATH "${TARGET_DIR}/${EXE_FILE}")
set(PACKAGE_DIR "${BIN_ROOT}/${PACKAGE_NAME}")
set(ARCHIVE_PATH "${BIN_ROOT}/${PACKAGE_NAME}.zip")
set(TARGET_RESOURCES_DIR "${TARGET_DIR}/resources")
set(BIN_RESOURCES_DIR "${BIN_ROOT}/resources")
file(MAKE_DIRECTORY "${BIN_ROOT}")
file(REMOVE_RECURSE "${PACKAGE_DIR}")
file(REMOVE "${ARCHIVE_PATH}")
file(MAKE_DIRECTORY "${PACKAGE_DIR}")
if(EXISTS "${EXE_PATH}")
file(COPY "${EXE_PATH}" DESTINATION "${PACKAGE_DIR}")
else()
message(FATAL_ERROR "Executable not found: ${EXE_PATH}")
endif()
if(EXISTS "${TARGET_RESOURCES_DIR}")
file(COPY "${TARGET_RESOURCES_DIR}" DESTINATION "${PACKAGE_DIR}")
elseif(EXISTS "${BIN_RESOURCES_DIR}")
file(COPY "${BIN_RESOURCES_DIR}" DESTINATION "${PACKAGE_DIR}")
endif()
execute_process(
COMMAND "${CMAKE_COMMAND}" -E tar cf "${PACKAGE_NAME}.zip" --format=zip "${PACKAGE_NAME}"
WORKING_DIRECTORY "${BIN_ROOT}"
RESULT_VARIABLE ZIP_RESULT
)
if(NOT ZIP_RESULT EQUAL 0)
message(FATAL_ERROR "Failed creating ${ARCHIVE_PATH}")
endif()

1
external/libanm2 vendored

Submodule external/libanm2 deleted from 623c67edbd

1
external/physfs vendored Submodule

Submodule external/physfs added at d70c3fcf06

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 71 KiB

1
snivy.rc Normal file
View File

@@ -0,0 +1 @@
IDI_ICON1 ICON DISCARDABLE "Icon.ico"

View File

@@ -1,273 +0,0 @@
#include "character.h"
#include <format>
#include "types.h"
#include "util/math_.h"
using namespace game::util;
using namespace game::anm2;
using namespace glm;
namespace game
{
float Character::max_capacity() { return capacity * CAPACITY_OVERSTUFFED_LIMIT_MULTIPLIER; }
float Character::over_capacity_calories_get()
{
if (calories < capacity) return 0.0f;
return (calories - capacity);
}
float Character::over_capacity_percent_get()
{
if (calories < capacity) return 0.0f;
return (calories - capacity) / (max_capacity() - capacity);
}
float Character::digestion_rate_second_get() { return digestionRate * 60; }
bool Character::is_over_capacity() { return calories + 1 >= capacity; }
bool Character::is_max_capacity() { return calories >= max_capacity(); }
void Character::talk()
{
if (auto talkItem = item_get(anm2::LAYER, talkLayerID))
{
talkOverride = {.animationIndex = animationIndex,
.sourceID = talkLayerID,
.destinationID = mouthLayerID,
.length = (float)item_length(talkItem),
.isLoop = true};
}
else
talkOverride.isEnabled = false;
}
void Character::blink()
{
if (auto blinkItem = item_get(anm2::LAYER, blinkLayerID))
{
blinkOverride = {.animationIndex = animationIndex,
.sourceID = blinkLayerID,
.destinationID = headLayerID,
.length = (float)item_length(blinkItem)};
}
else
blinkOverride.isEnabled = false;
}
float Character::weight_get(MeasurementSystem system) { return system == IMPERIAL ? weight * KG_TO_LB : weight; }
float Character::weight_threshold_get(int stage, MeasurementSystem system)
{
stage = glm::clamp(stage, 0, WEIGHT_STAGE_MAX);
return system == IMPERIAL ? WEIGHT_THRESHOLDS[stage] * KG_TO_LB : WEIGHT_THRESHOLDS[stage];
}
float Character::weight_threshold_current_get(MeasurementSystem system)
{
return weight_threshold_get(weightStage, system);
}
float Character::weight_threshold_next_get(MeasurementSystem system)
{
auto nextStage = glm::clamp(0, weightStage + 1, WEIGHT_STAGE_MAX);
return weight_threshold_get(nextStage, system);
}
float Character::progress_to_next_weight_threshold_get()
{
if (weightStage >= WEIGHT_STAGE_MAX - 1) return 0.0f;
return (weight - weight_threshold_current_get()) / (weight_threshold_next_get() - weight_threshold_current_get());
}
vec4 Character::mouth_rect_get() { return null_frame_rect(mouthNullID); }
vec4 Character::head_rect_get() { return null_frame_rect(headNullID); }
vec4 Character::belly_rect_get() { return null_frame_rect(bellyNullID); }
vec4 Character::tail_rect_get() { return null_frame_rect(tailNullID); }
Character::Character(Anm2* _anm2, glm::ivec2 _position) : Actor(_anm2, _position)
{
talkLayerID = item_id_get(LAYER_TALK);
blinkLayerID = item_id_get(LAYER_BLINK);
headLayerID = item_id_get(LAYER_HEAD);
mouthLayerID = item_id_get(LAYER_MOUTH);
torsoLayerID = item_id_get(LAYER_TORSO);
tailLayerID = item_id_get(LAYER_TAIL);
mouthNullID = item_id_get(NULL_MOUTH, anm2::NULL_);
headNullID = item_id_get(NULL_HEAD, anm2::NULL_);
bellyNullID = item_id_get(NULL_BELLY, anm2::NULL_);
tailNullID = item_id_get(NULL_TAIL, anm2::NULL_);
torsoCapacityScale = {.destinationID = torsoLayerID, .mode = Override::FRAME_ADD};
tailCapacityScale = {.destinationID = tailLayerID, .mode = Override::FRAME_ADD};
torsoCapacityScale.frame.scale = glm::vec2();
tailCapacityScale.frame.scale = glm::vec2();
overrides.emplace_back(&talkOverride);
overrides.emplace_back(&blinkOverride);
overrides.emplace_back(&torsoCapacityScale);
overrides.emplace_back(&tailCapacityScale);
}
void Character::digestion_start()
{
isDigesting = true;
digestionTimer = DIGESTION_TIMER_MAX;
isJustDigestionStart = true;
}
void Character::digestion_end()
{
auto increment = calories * CALORIE_TO_KG;
weight += increment;
totalWeightGained += increment;
isForceStageUp = false;
if (is_over_capacity()) capacity += over_capacity_percent_get() * capacity * CAPACITY_OVER_BONUS;
calories = 0;
digestionProgress = 0;
digestionTimer = 0;
digestionCount++;
isDigesting = false;
isJustDigestionEnd = true;
}
void Character::tick()
{
Actor::tick();
isJustDigestionStart = false;
isJustDigestionEnd = false;
isJustStageUp = false;
isJustFinalThreshold = false;
auto animation = animation_get();
if (animation && !animation->isLoop && !isPlaying)
{
if (state == APPEAR) isJustAppeared = true;
if (state == STAGE_UP && weightStage == WEIGHT_STAGE_MAX - 1 && !isForceStageUp) isJustFinalThreshold = true;
state_set(IDLE, true);
}
if (isDigesting)
{
digestionTimer--;
if (digestionTimer <= 0) digestion_end();
}
else if (calories > 0)
{
digestionProgress += digestionRate;
if (digestionProgress >= DIGESTION_MAX) digestion_start();
}
if (math::random_percent_roll(BLINK_CHANCE)) blink();
auto progress = calories / max_capacity();
auto weightPercent = progress_to_next_weight_threshold_get();
auto capacityPercent = isDigesting ? ((float)digestionTimer / DIGESTION_TIMER_MAX) * progress : progress;
auto scaleBonus =
vec2(glm::min(SCALE_BONUS_MAX * ((capacityPercent * 0.5f) + (weightPercent * 0.5f)), SCALE_BONUS_MAX));
torsoCapacityScale.frame.scale = glm::max(vec2(), scaleBonus);
tailCapacityScale.frame.scale = glm::max(vec2(), scaleBonus);
if (!isForceStageUp)
{
for (int i = 0; i < WEIGHT_STAGE_MAX; i++)
{
if (weight >= WEIGHT_THRESHOLDS[i])
{
if (i == previousWeightStage + 1)
{
state_set(STAGE_UP);
isJustStageUp = true;
weightStage = i;
break;
}
}
else if (weight < WEIGHT_THRESHOLDS[i])
break;
}
}
if (weight > highestWeight) highestWeight = weight;
previousWeightStage = weightStage;
}
std::string Character::animation_name_convert(const std::string& name)
{
return std::format("{}{}", name, weightStage);
}
void Character::state_set(State state, bool isForce)
{
if (this->state == state && !isForce) return;
this->state = state;
AnimationType type{ANIMATION_NEUTRAL};
auto speedMultiplier = 1.0f;
switch (this->state)
{
case IDLE:
if (is_over_capacity())
type = ANIMATION_NEUTRAL_FULL;
else
type = ANIMATION_NEUTRAL;
break;
case EAGER:
type = ANIMATION_EAGER;
break;
case CRY:
type = ANIMATION_CRY;
break;
case SHOCKED:
type = ANIMATION_SHOCKED;
break;
case EAT:
type = ANIMATION_EAT;
speedMultiplier = eatSpeedMultiplier;
break;
case ANGRY:
type = ANIMATION_ANGRY;
break;
case PAT:
type = ANIMATION_PAT;
break;
case BURP_SMALL:
type = ANIMATION_BURP_SMALL;
break;
case BURP_BIG:
type = ANIMATION_BURP_BIG;
break;
case HEAD_RUB:
if (is_over_capacity())
type = ANIMATION_HEAD_RUB_FULL;
else
type = ANIMATION_HEAD_RUB;
break;
case BELLY_RUB:
if (is_over_capacity())
type = ANIMATION_BELLY_RUB_FULL;
else
type = ANIMATION_BELLY_RUB;
break;
case TAIL_RUB:
if (is_over_capacity())
type = ANIMATION_TAIL_RUB_FULL;
else
type = ANIMATION_TAIL_RUB;
break;
case STAGE_UP:
type = ANIMATION_STAGE_UP;
break;
default:
break;
};
play(animation_name_convert(ANIMATIONS[type]), PLAY, 0.0f, speedMultiplier);
}
}

View File

@@ -1,185 +0,0 @@
#pragma once
#include "resource/actor.h"
#include "types.h"
namespace game
{
class Character : public resource::Actor
{
public:
static constexpr auto LAYER_TAIL = "Tail";
static constexpr auto LAYER_TORSO = "Torso";
static constexpr auto LAYER_HEAD = "Head";
static constexpr auto LAYER_MOUTH = "Mouth";
static constexpr auto LAYER_TALK = "Talk";
static constexpr auto LAYER_BLINK = "Blink";
static constexpr auto NULL_MOUTH = "Mouth";
static constexpr auto NULL_HEAD = "Head";
static constexpr auto NULL_BELLY = "Belly";
static constexpr auto NULL_TAIL = "Tail";
static constexpr auto EVENT_EAT = "Eat";
static constexpr auto BLINK_CHANCE = 0.5f;
static constexpr auto PAT_CHANCE = 25.0f;
static constexpr auto BURP_SMALL_CHANCE = 20.0f;
static constexpr auto BURP_BIG_CHANCE = 10.0f;
static constexpr auto GURGLE_CHANCE = 0.1f;
static constexpr auto GURGLE_CHANCE_BONUS = 0.3f;
static constexpr auto CAPACITY_BASE = 500.0f;
static constexpr auto CALORIE_TO_KG = 1.0 / 1000.0f;
static constexpr auto CAPACITY_OVERSTUFFED_LIMIT_MULTIPLIER = 1.5f;
static constexpr auto SCALE_BONUS_MAX = 10.0f;
static constexpr auto PAT_LENGTH = 5;
static constexpr auto PAT_SCALE_RANGE = 5;
static constexpr auto EAT_SPEED_MULTIPLIER_MIN = 1.0f;
static constexpr auto EAT_SPEED_MULTIPLIER_MAX = 3.0f;
static constexpr auto DIGESTION_RATE_MIN = 0.00f;
static constexpr auto DIGESTION_RATE_MAX = 0.25f;
static constexpr auto DIGESTION_RATE_BASE = 0.05f;
static constexpr auto DIGESTION_MAX = 100.0f;
static constexpr auto DIGESTION_TIMER_MAX = 60;
static constexpr auto DIGESTION_RUB_BONUS = 0.01f;
static constexpr auto CAPACITY_OVER_BONUS = 0.1f;
static constexpr auto WEIGHT_STAGE_MAX = 5;
static constexpr float WEIGHT_THRESHOLDS[] = {
8.1f, 15.0f, 30.0f, 50.0f, 75.0f,
};
static constexpr auto MOUTH_SIZE = glm::vec2(50.0f, 50.0f);
enum State
{
APPEAR,
IDLE,
EAGER,
SHOCKED,
EAT,
CRY,
ANGRY,
BURP_SMALL,
BURP_BIG,
PAT,
HEAD_RUB,
BELLY_RUB,
TAIL_RUB,
STAGE_UP
};
#define ANIMATIONS_LIST \
X(ANIMATION_NEUTRAL, "Neutral") \
X(ANIMATION_NEUTRAL_FULL, "NeutralFull") \
X(ANIMATION_SHOCKED, "Shocked") \
X(ANIMATION_EAT, "Eat") \
X(ANIMATION_ANGRY, "Angry") \
X(ANIMATION_EAGER, "Eager") \
X(ANIMATION_CRY, "Cry") \
X(ANIMATION_PAT, "Pat") \
X(ANIMATION_BURP_SMALL, "BurpSmall") \
X(ANIMATION_BURP_BIG, "BurpBig") \
X(ANIMATION_HEAD_RUB, "HeadRub") \
X(ANIMATION_HEAD_RUB_FULL, "HeadRubFull") \
X(ANIMATION_BELLY_RUB, "BellyRub") \
X(ANIMATION_BELLY_RUB_FULL, "BellyRubFull") \
X(ANIMATION_TAIL_RUB, "TailRub") \
X(ANIMATION_TAIL_RUB_FULL, "TailRubFull") \
X(ANIMATION_STAGE_UP, "StageUp")
enum AnimationType
{
#define X(symbol, string) symbol,
ANIMATIONS_LIST
#undef X
};
static constexpr const char* ANIMATIONS[] = {
#define X(symbol, string) string,
ANIMATIONS_LIST
#undef X
};
float weight{WEIGHT_THRESHOLDS[0]};
int weightStage{0};
int previousWeightStage{0};
float highestWeight{};
float calories{};
float capacity{CAPACITY_BASE};
float digestionProgress{};
float digestionRate{DIGESTION_RATE_BASE};
float totalWeightGained{};
float totalCaloriesConsumed{};
int foodItemsEaten{};
int digestionCount{};
bool isJustDigestionStart{};
bool isJustDigestionEnd{};
bool isJustStageUp{};
bool isForceStageUp{};
bool isJustFinalThreshold{};
bool isFinalThresholdReached{};
bool isDigesting{};
bool isJustAppeared{};
int digestionTimer{};
int blinkLayerID{-1};
int headLayerID{-1};
int tailLayerID{-1};
int talkLayerID{-1};
int mouthLayerID{-1};
int torsoLayerID{-1};
int mouthNullID{-1};
int headNullID{-1};
int bellyNullID{-1};
int tailNullID{-1};
bool isFinishedFood{};
float eatSpeedMultiplier{EAT_SPEED_MULTIPLIER_MIN};
State state{APPEAR};
Override blinkOverride{};
Override talkOverride{};
Override torsoCapacityScale{};
Override tailCapacityScale{};
Character(anm2::Anm2*, glm::ivec2);
void talk();
void blink();
void tick();
void digestion_start();
void digestion_end();
void state_set(State, bool = false);
glm::vec4 mouth_rect_get();
glm::vec4 belly_rect_get();
glm::vec4 head_rect_get();
glm::vec4 tail_rect_get();
float weight_get(MeasurementSystem = METRIC);
float weight_threshold_get(int, MeasurementSystem = METRIC);
float weight_threshold_current_get(MeasurementSystem = METRIC);
float weight_threshold_next_get(MeasurementSystem = METRIC);
float progress_to_next_weight_threshold_get();
float over_capacity_percent_get();
float over_capacity_calories_get();
float digestion_rate_second_get();
bool is_max_capacity();
bool is_over_capacity();
float max_capacity();
std::string animation_name_convert(const std::string&);
};
}

View File

@@ -1,13 +0,0 @@
#include "cursor.h"
#include "util/imgui_.h"
using namespace game::util;
using namespace game::anm2;
using namespace glm;
namespace game
{
Cursor::Cursor(Anm2* anm2) : Actor(anm2, glm::vec2()) {}
void Cursor::update() { position = imgui::to_vec2(ImGui::GetMousePos()); }
}

View File

@@ -1,19 +0,0 @@
#pragma once
#include "resource/actor.h"
namespace game
{
class Cursor : public resource::Actor
{
public:
static constexpr const char* ANIMATION_DEFAULT = "Default";
static constexpr const char* ANIMATION_HOVER = "Hover";
static constexpr const char* ANIMATION_GRAB = "Grab";
static constexpr const char* ANIMATION_RUB = "Rub";
Cursor(anm2::Anm2* anm2);
void update();
};
}

456
src/entity/actor.cpp Normal file
View File

@@ -0,0 +1,456 @@
#include "actor.hpp"
#include "../util/map.hpp"
#include "../util/math.hpp"
#include "../util/unordered_map.hpp"
#include "../util/vector.hpp"
#include <cstddef>
#include <glm/glm.hpp>
#include "../log.hpp"
#include <imgui.h>
using namespace glm;
using namespace game::util;
using namespace game::resource::xml;
namespace game::entity
{
Actor::Override::Override(int _id, Anm2::Type _type, Actor::Override::Mode _mode, FrameOptional _frame,
std::optional<float> _time, Actor::Override::Function _function, float _cycles)
: id(_id), type(_type), mode(_mode), frame(_frame), time(_time), function(_function), cycles(_cycles)
{
frameBase = _frame;
timeStart = _time;
}
Actor::Actor(const Actor&) = default;
Actor::Actor(Actor&&) noexcept = default;
Actor& Actor::operator=(const Actor&) = default;
Actor& Actor::operator=(Actor&&) noexcept = default;
Actor::Actor(Anm2 _anm2, vec2 _position, Mode playMode, float startAtTime, int startAnimationIndex)
: Anm2(_anm2), position(_position)
{
this->mode = playMode;
this->startTime = startAtTime;
if (startAnimationIndex != -1)
play(startAnimationIndex, playMode, startAtTime);
else
play_default_animation(playMode, startAtTime);
}
Anm2::Animation* Actor::animation_get(int index)
{
if (index == -1) index = animationIndex;
if (animationMapReverse.contains(index)) return &animations[index];
return nullptr;
}
Anm2::Animation* Actor::animation_get(const std::string& name)
{
if (animationMap.contains(name)) return &animations[animationMap[name]];
return nullptr;
}
bool Actor::is_playing(const std::string& name)
{
if (name.empty())
return state == PLAYING;
else
return state == PLAYING && animationMap[name] == animationIndex;
}
int Actor::animation_index_get(const std::string& name)
{
if (animationMap.contains(name)) return animationMap[name];
return -1;
}
Anm2::Item* Actor::item_get(Anm2::Type type, int id, int checkAnimationIndex)
{
if (checkAnimationIndex == -1) checkAnimationIndex = this->animationIndex;
if (auto animation = animation_get(checkAnimationIndex))
{
switch (type)
{
case Anm2::ROOT:
return &animation->rootAnimation;
break;
case Anm2::LAYER:
return unordered_map::find(animation->layerAnimations, id);
case Anm2::NULL_:
return map::find(animation->nullAnimations, id);
break;
case Anm2::TRIGGER:
return &animation->triggers;
default:
return nullptr;
}
}
return nullptr;
}
int Actor::item_length(Anm2::Item* item)
{
if (!item) return -1;
int duration{};
for (auto& frame : item->frames)
duration += frame.duration;
return duration;
}
Anm2::Frame Actor::frame_generate(Anm2::Item& item, float frameTime, Anm2::Type type, int id)
{
Anm2::Frame frame{};
frame.isVisible = false;
if (item.frames.empty()) return frame;
frameTime = frameTime < 0.0f ? 0.0f : frameTime;
Anm2::Frame* frameNext = nullptr;
Anm2::Frame frameNextCopy{};
int durationCurrent = 0;
int durationNext = 0;
for (int i = 0; i < (int)item.frames.size(); i++)
{
Anm2::Frame& checkFrame = item.frames[i];
frame = checkFrame;
durationNext += frame.duration;
if (frameTime >= durationCurrent && frameTime < durationNext)
{
if (i + 1 < (int)item.frames.size())
{
frameNext = &item.frames[i + 1];
frameNextCopy = *frameNext;
}
else
frameNext = nullptr;
break;
}
durationCurrent += frame.duration;
}
auto override_handle = [&](Anm2::Frame& overrideFrame)
{
for (auto& override : overrides)
{
if (override.type != type) continue;
if (override.id != id) continue;
auto& source = override.frame;
switch (override.mode)
{
case Override::SET:
if (source.position.has_value()) overrideFrame.position = *source.position;
if (source.pivot.has_value()) overrideFrame.pivot = *source.pivot;
if (source.size.has_value()) overrideFrame.size = *source.size;
if (source.scale.has_value()) overrideFrame.scale = *source.scale;
if (source.crop.has_value()) overrideFrame.crop = *source.crop;
if (source.rotation.has_value()) overrideFrame.rotation = *source.rotation;
if (source.tint.has_value()) overrideFrame.tint = *source.tint;
if (source.colorOffset.has_value()) overrideFrame.colorOffset = *source.colorOffset;
if (source.isInterpolated.has_value()) overrideFrame.isInterpolated = *source.isInterpolated;
if (source.isVisible.has_value()) overrideFrame.isVisible = *source.isVisible;
break;
case Override::ADD:
if (source.scale.has_value()) overrideFrame.scale += *source.scale;
break;
default:
break;
}
}
};
override_handle(frame);
if (frameNext) override_handle(frameNextCopy);
if (frame.isInterpolated && frameNext && frame.duration > 1)
{
auto interpolation = (frameTime - durationCurrent) / (durationNext - durationCurrent);
frame.rotation = glm::mix(frame.rotation, frameNextCopy.rotation, interpolation);
frame.position = glm::mix(frame.position, frameNextCopy.position, interpolation);
frame.scale = glm::mix(frame.scale, frameNextCopy.scale, interpolation);
frame.colorOffset = glm::mix(frame.colorOffset, frameNextCopy.colorOffset, interpolation);
frame.tint = glm::mix(frame.tint, frameNextCopy.tint, interpolation);
}
return frame;
}
void Actor::play(int index, Mode playMode, float startAtTime, float speedMultiplierValue)
{
if (!vector::in_bounds(animations, index)) return;
if (playMode != PLAY_FORCE && index == animationIndex) return;
this->playedEventID = -1;
this->playedTriggers.clear();
this->speedMultiplier = speedMultiplierValue;
this->animationIndex = index;
this->time = startAtTime;
if (playMode == PLAY) state = PLAYING;
}
void Actor::queue_play(QueuedPlay newQueuedPlay) { queuedPlay = newQueuedPlay; }
void Actor::queue_default_animation() { queue_play({defaultAnimation}); }
void Actor::play(const std::string& name, Mode playMode, float startAtTime, float speedMultiplierValue)
{
if (animationMap.contains(name))
play(animationMap.at(name), playMode, startAtTime, speedMultiplierValue);
else
logger.error(std::string("Animation \"" + name + "\" does not exist! Unable to play!"));
}
void Actor::play_default_animation(Mode playMode, float startAtTime, float speedMultiplierValue)
{
play(defaultAnimationID, playMode, startAtTime, speedMultiplierValue);
}
void Actor::tick()
{
if (state == Actor::STOPPED)
{
if (!nextQueuedPlay.empty())
{
queuedPlay = nextQueuedPlay;
queuedPlay.isPlayAfterAnimation = false;
nextQueuedPlay = QueuedPlay{};
}
currentQueuedPlay = QueuedPlay{};
}
if (auto animation = animation_get(); animation && animation->isLoop) currentQueuedPlay = QueuedPlay{};
if (!queuedPlay.empty())
{
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{};
}
auto animation = animation_get();
if (!animation || animation->frameNum == 1 || mode == SET || state == STOPPED) return;
playedEventID = -1;
for (auto& trigger : animation->triggers.frames)
{
if (!playedTriggers.contains(trigger.atFrame) && time >= trigger.atFrame)
{
auto id = trigger.soundIDs[(int)math::random_max((float)trigger.soundIDs.size())];
if (auto sound = map::find(sounds, id)) sound->audio.play();
playedTriggers.insert((int)trigger.atFrame);
playedEventID = trigger.eventID;
}
}
auto increment = (fps / TICK_RATE) * speedMultiplier;
time += increment;
if (time >= animation->frameNum)
{
if (animation->isLoop)
time = 0.0f;
else
state = STOPPED;
playedTriggers.clear();
}
for (int i = 0; i < (int)overrides.size();)
{
auto& override_ = overrides[i];
if (override_.function) override_.function(override_);
if (override_.time.has_value())
{
*override_.time -= 1.0f;
if (*override_.time <= 0.0f)
{
overrides.erase(overrides.begin() + i);
continue;
}
}
i++;
}
}
glm::vec4 Actor::null_frame_rect(int nullID)
{
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
if (nullID == -1) return glm::vec4(NAN);
auto item = item_get(Anm2::NULL_, nullID);
if (!item) return glm::vec4(NAN);
auto animation = animation_get();
if (!animation) return glm::vec4(NAN);
auto root = frame_generate(animation->rootAnimation, time, Anm2::ROOT);
auto frame = frame_generate(*item, time, Anm2::NULL_, nullID);
if (!frame.isVisible) return glm::vec4(NAN);
auto rootModel =
math::quad_model_no_size_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
auto frameModel = math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, vec2(1.0f), frame.rotation);
auto model = rootModel * frameModel;
float minX = std::numeric_limits<float>::infinity();
float minY = std::numeric_limits<float>::infinity();
float maxX = -std::numeric_limits<float>::infinity();
float maxY = -std::numeric_limits<float>::infinity();
for (auto& corner : CORNERS)
{
vec4 world = model * vec4(corner, 0.0f, 1.0f);
minX = std::min(minX, world.x);
minY = std::min(minY, world.y);
maxX = std::max(maxX, world.x);
maxY = std::max(maxY, world.y);
}
return glm::vec4(minX, minY, maxX - minX, maxY - minY);
}
void Actor::render(resource::Shader& textureShader, resource::Shader& rectShader, Canvas& canvas)
{
auto animation = animation_get();
if (!animation) return;
auto root = frame_generate(animation->rootAnimation, time, Anm2::ROOT);
auto rootModel =
math::quad_model_no_size_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
for (auto& i : animation->layerOrder)
{
auto& layerAnimation = animation->layerAnimations[i];
if (!layerAnimation.isVisible) continue;
auto layer = map::find(layers, i);
if (!layer) continue;
auto spritesheet = map::find(spritesheets, layer->spritesheetID);
if (!spritesheet) continue;
auto frame = frame_generate(layerAnimation, time, Anm2::LAYER, i);
if (!frame.isVisible) continue;
auto model =
math::quad_model_get(frame.size, frame.position, frame.pivot, math::to_unit(frame.scale), frame.rotation);
model = rootModel * model;
auto& texture = spritesheet->texture;
if (!texture.is_valid()) return;
auto tint = frame.tint * root.tint;
auto colorOffset = frame.colorOffset + root.colorOffset;
auto inset = vec2(0);
auto uvMin = (frame.crop + inset) / vec2(texture.size);
auto uvMax = (frame.crop + frame.size - inset) / vec2(texture.size);
auto uvVertices = math::uv_vertices_get(uvMin, uvMax);
canvas.texture_render(textureShader, texture.id, model, tint, colorOffset, uvVertices.data());
}
if (isShowNulls)
{
for (int i = 0; i < (int)animation->nullAnimations.size(); i++)
{
auto& nullAnimation = animation->nullAnimations[i];
if (!nullAnimation.isVisible) continue;
auto frame = frame_generate(nullAnimation, time, Anm2::NULL_, i);
if (!frame.isVisible) continue;
auto model = math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, vec2(1.0f), frame.rotation);
model = rootModel * model;
canvas.rect_render(rectShader, model);
}
}
}
vec4 Actor::rect()
{
constexpr ivec2 CORNERS[4] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
auto animation = animation_get();
float minX = std::numeric_limits<float>::infinity();
float minY = std::numeric_limits<float>::infinity();
float maxX = -std::numeric_limits<float>::infinity();
float maxY = -std::numeric_limits<float>::infinity();
bool any = false;
if (!animation) return vec4(-NAN);
for (float t = 0.0f; t < (float)animation->frameNum; t += 1.0f)
{
mat4 transform(1.0f);
auto root = frame_generate(animation->rootAnimation, t, Anm2::ROOT);
transform *=
math::quad_model_no_size_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
for (auto& [id, layerAnimation] : animation->layerAnimations)
{
if (!layerAnimation.isVisible) continue;
auto frame = frame_generate(layerAnimation, t, Anm2::LAYER, id);
if (frame.size == vec2() || !frame.isVisible) continue;
auto layerTransform = transform * math::quad_model_get(frame.size, frame.position, frame.pivot,
math::to_unit(frame.scale), frame.rotation);
for (auto& corner : CORNERS)
{
vec4 world = layerTransform * vec4(corner, 0.0f, 1.0f);
minX = std::min(minX, world.x);
minY = std::min(minY, world.y);
maxX = std::max(maxX, world.x);
maxY = std::max(maxY, world.y);
any = true;
}
}
}
if (!any) return vec4(-NAN);
return {minX, minY, maxX - minX, maxY - minY};
}
bool Actor::is_animation_finished()
{
if (auto animation = animation_get())
{
if (animation->isLoop) return true;
if (time > animation->frameNum) return true;
}
return false;
}
void Actor::consume_played_event() { playedEventID = -1; }
};

113
src/entity/actor.hpp Normal file
View File

@@ -0,0 +1,113 @@
#pragma once
#include <unordered_set>
#include "../render/canvas.hpp"
#include "../resource/xml/anm2.hpp"
namespace game::entity
{
class Actor : public resource::xml::Anm2
{
public:
static constexpr auto TICK_RATE = 30.0f;
enum Mode
{
PLAY,
PLAY_FORCE,
SET,
};
enum State
{
STOPPED,
PLAYING
};
class Override
{
private:
public:
enum Mode
{
SET,
ADD
};
using Function = void (*)(Override&);
int id{-1};
Anm2::Type type{Anm2::NONE};
Mode mode{SET};
FrameOptional frame{};
std::optional<float> time{};
Function function{nullptr};
FrameOptional frameBase{};
std::optional<float> timeStart{};
float cycles{};
Override() = default;
Override(int, Anm2::Type, Mode, FrameOptional = {}, std::optional<float> = std::nullopt, Function = nullptr,
float = 0);
};
struct QueuedPlay
{
std::string animation{};
float time{};
float speedMultiplier{1.0f};
Mode mode{PLAY};
bool isInterruptible{true};
bool isPlayAfterAnimation{false};
inline bool empty() { return animation.empty(); };
};
State state{STOPPED};
Mode mode{PLAY};
glm::vec2 position{};
float time{};
bool isShowNulls{};
int animationIndex{-1};
int playedEventID{-1};
float startTime{};
float speedMultiplier{};
QueuedPlay queuedPlay{};
QueuedPlay currentQueuedPlay{};
QueuedPlay nextQueuedPlay{};
std::unordered_set<int> playedTriggers{};
std::vector<Override> overrides{};
Actor() = default;
Actor(const Actor&);
Actor(Actor&&) noexcept;
Actor& operator=(const Actor&);
Actor& operator=(Actor&&) noexcept;
Actor(resource::xml::Anm2, glm::vec2 position = {}, Mode = PLAY, float time = 0.0f, int animationIndex = -1);
bool is_playing(const std::string& name = {});
glm::vec4 null_frame_rect(int = -1);
glm::vec4 rect();
int animation_index_get(const std::string&);
int item_length(resource::xml::Anm2::Item*);
resource::xml::Anm2::Animation* animation_get(const std::string&);
resource::xml::Anm2::Animation* animation_get(int = -1);
resource::xml::Anm2::Frame frame_generate(resource::xml::Anm2::Item&, float, resource::xml::Anm2::Type,
int id = -1);
resource::xml::Anm2::Item* item_get(resource::xml::Anm2::Type, int = -1, int = -1);
void consume_played_event();
void play(const std::string& animation, Mode = PLAY, float time = 0.0f, float speedMultiplier = 1.0f);
void play(int index, Mode = PLAY, float time = 0.0f, float speedMultiplier = 1.0f);
void play_default_animation(Mode = PLAY, float = 0.0f, float = 1.0f);
void queue_default_animation();
void queue_play(QueuedPlay);
bool is_animation_finished();
void render(resource::Shader& textureShader, resource::Shader& rectShader, Canvas&);
void tick();
};
}

376
src/entity/character.cpp Normal file
View File

@@ -0,0 +1,376 @@
#include "character.hpp"
#include <format>
#include "../util/math.hpp"
#include "../util/vector.hpp"
using namespace game::util;
using namespace glm;
namespace game::entity
{
Character::Character(const Character&) = default;
Character::Character(Character&&) noexcept = default;
Character& Character::operator=(const Character&) = default;
Character& Character::operator=(Character&&) noexcept = default;
Character::Character(resource::xml::Character& _data, glm::ivec2 _position) : Actor(_data.anm2, _position)
{
data = _data;
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;
calories = saveIsValid ? save.calories : 0;
isDigesting = saveIsValid ? save.isDigesting : false;
digestionProgress = saveIsValid ? save.digestionProgress : 0;
digestionTimer = saveIsValid ? save.digestionTimer : 0;
auto& talkSource = data.talkOverride.layerSource;
auto& talkDestination = data.talkOverride.layerDestination;
talkOverrideID = vector::push_index(overrides, Actor::Override(talkDestination, Anm2::LAYER, Override::SET));
for (auto& animation : animations)
{
if (!animation.layerAnimations.contains(talkSource))
animationTalkDurations.emplace_back(-1);
else
animationTalkDurations.emplace_back(item_length(&animation.layerAnimations.at(talkSource)));
}
auto& blinkSource = data.blinkOverride.layerSource;
auto& blinkDestination = data.blinkOverride.layerDestination;
blinkOverrideID = vector::push_index(overrides, Actor::Override(blinkDestination, Anm2::LAYER, Override::SET));
for (auto& animation : animations)
{
if (!animation.layerAnimations.contains(blinkSource))
animationBlinkDurations.emplace_back(-1);
else
animationBlinkDurations.emplace_back(item_length(&animation.layerAnimations.at(blinkSource)));
}
for (int i = 0; i < (int)data.expandAreas.size(); i++)
{
auto& expandArea = data.expandAreas[i];
expandAreaOverrideLayerIDs[i] =
vector::push_index(overrides, Actor::Override(expandArea.layerID, Anm2::LAYER, Override::ADD));
expandAreaOverrideNullIDs[i] =
vector::push_index(overrides, Actor::Override(expandArea.nullID, Anm2::NULL_, Override::ADD));
}
for (int i = 0; i < (int)data.interactAreas.size(); i++)
{
auto& interactArea = data.interactAreas[i];
if (interactArea.layerID != -1)
interactAreaOverrides[i] = Actor::Override(interactArea.layerID, Anm2::LAYER, Override::ADD);
}
stage = stage_get();
expand_areas_apply();
}
float Character::weight_get(measurement::System system)
{
return system == measurement::IMPERIAL ? weight * (float)measurement::KG_TO_LB : weight;
}
int Character::stage_from_weight_get(float checkWeight) const
{
if (data.stages.empty()) return 0;
if (checkWeight <= data.weight) return 0;
for (int i = 0; i < (int)data.stages.size(); i++)
if (checkWeight < data.stages[i].threshold) return i;
return stage_max_get();
}
int Character::stage_get() const { return stage_from_weight_get(weight); }
int Character::stage_max_get() const { return (int)data.stages.size(); }
float Character::stage_threshold_get(int stageIndex, measurement::System system) const
{
if (stageIndex == -1) stageIndex = this->stage;
float threshold = data.weight;
if (!data.stages.empty())
{
if (stageIndex <= 0)
threshold = data.weight;
else if (stageIndex >= stage_max_get())
threshold = data.stages.back().threshold;
else
threshold = data.stages[stageIndex - 1].threshold;
}
return system == measurement::IMPERIAL ? threshold * (float)measurement::KG_TO_LB : threshold;
}
float Character::stage_threshold_next_get(measurement::System system) const
{
return stage_threshold_get(stage + 1, system);
}
float Character::stage_progress_get()
{
auto currentStage = stage_get();
if (currentStage >= stage_max_get()) return 1.0f;
auto currentThreshold = stage_threshold_get(currentStage);
auto nextThreshold = stage_threshold_get(currentStage + 1);
if (nextThreshold <= currentThreshold) return 1.0f;
return (weight - currentThreshold) / (nextThreshold - currentThreshold);
}
float Character::digestion_rate_get() { return digestionRate * 60; }
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); }
void Character::play_convert(const std::string& animation, Mode playMode, float startAtTime,
float speedMultiplierValue)
{
play(animation_name_convert(animation), playMode, startAtTime, speedMultiplierValue);
}
void Character::expand_areas_apply()
{
auto stageProgress = stage_progress_get();
auto capacityProgress = isDigesting
? (float)calories / max_capacity() * (float)digestionTimer / data.digestionTimerMax
: calories / max_capacity();
for (int i = 0; i < (int)data.expandAreas.size(); i++)
{
auto& expandArea = data.expandAreas[i];
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 scaleAdd =
glm::clamp(glm::vec2(), glm::vec2(stageScaleAdd + capacityScaleAdd), glm::vec2(expandArea.scaleAdd));
overrideLayer.frame.scale = scaleAdd;
overrideNull.frame.scale = scaleAdd;
}
}
void Character::update()
{
isJustStageUp = false;
isJustStageFinal = false;
isJustDigested = false;
}
void Character::tick()
{
if (state == Actor::STOPPED)
{
if (isStageUp)
{
if (stage >= (int)data.stages.size())
isJustStageFinal = true;
else
isJustStageUp = true;
isStageUp = false;
}
if (nextQueuedPlay.empty() && !isTalking) queue_idle_animation();
}
Actor::tick();
if (isDigesting)
{
digestionTimer--;
if (digestionTimer <= 0)
{
auto increment = calories * data.caloriesToKilogram;
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);
}
totalCaloriesConsumed += calories;
calories = 0;
if (auto nextStage = stage_from_weight_get(weight + increment); nextStage > stage_from_weight_get(weight))
{
queuedPlay = QueuedPlay{};
nextQueuedPlay = QueuedPlay{};
currentQueuedPlay = QueuedPlay{};
queue_play({.animation = data.animations.stageUp, .isInterruptible = false});
stage = nextStage;
isStageUp = true;
}
else
isJustDigested = true;
weight += increment;
isDigesting = false;
digestionTimer = data.digestionTimerMax;
digestionProgress = 0;
}
}
else
{
if (calories > 0) digestionProgress += digestionRate;
if (digestionProgress >= DIGESTION_MAX)
{
isDigesting = true;
digestionTimer = data.digestionTimerMax;
data.sounds.digest.play();
}
}
if (math::random_percent_roll(
math::to_percent(data.gurgleChance * (capacity_percent_get() * data.gurgleCapacityMultiplier))))
data.sounds.gurgle.play();
stage = stage_get();
expand_areas_apply();
auto& talkOverride = overrides[talkOverrideID];
if (isTalking)
{
auto talk_reset = [&]()
{
isTalking = false;
talkTimer = 0.0f;
talkOverride.frame = FrameOptional();
};
auto& id = data.talkOverride.layerSource;
auto& layerAnimations = animation_get()->layerAnimations;
if (layerAnimations.contains(id) && animationTalkDurations.at(animationIndex) > -1)
{
auto& layerAnimation = layerAnimations.at(data.talkOverride.layerSource);
if (!layerAnimation.frames.empty())
{
auto frame = frame_generate(layerAnimation, talkTimer, Anm2::LAYER, id);
talkOverride.frame.crop = frame.crop;
talkOverride.frame.size = frame.size;
talkOverride.frame.pivot = frame.pivot;
talkTimer += 1.0f;
if (talkTimer > animationTalkDurations.at(animationIndex)) talkTimer = 0.0f;
}
else
talk_reset();
}
else
talk_reset();
}
else
talkOverride.frame = {};
auto& blinkOverride = overrides[blinkOverrideID];
if (auto blinkDuration = animationBlinkDurations[animationIndex]; blinkDuration != 1)
{
if (math::random_percent_roll(data.blinkChance)) isBlinking = true;
if (isBlinking)
{
auto blink_reset = [&]()
{
isBlinking = false;
blinkTimer = 0.0f;
blinkOverride.frame = FrameOptional();
};
auto& id = data.blinkOverride.layerSource;
auto& layerAnimations = animation_get()->layerAnimations;
if (layerAnimations.contains(id))
{
auto& layerAnimation = layerAnimations.at(data.blinkOverride.layerSource);
if (!layerAnimation.frames.empty())
{
auto frame = frame_generate(layerAnimation, blinkTimer, Anm2::LAYER, id);
blinkOverride.frame.crop = frame.crop;
blinkOverride.frame.size = frame.size;
blinkOverride.frame.pivot = frame.pivot;
blinkTimer += 1.0f;
if (blinkTimer >= blinkDuration) blink_reset();
}
else
blink_reset();
}
else
blink_reset();
}
}
}
void Character::queue_play(QueuedPlay play)
{
if (isStageUp) return;
queuedPlay = play;
queuedPlay.animation = animation_name_convert(queuedPlay.animation);
}
void Character::queue_idle_animation()
{
if (isStageUp) return;
if (data.animations.idle.empty()) return;
queue_play(
{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)
{
case NORMAL:
spritesheets.at(data.alternateSpritesheet.id).texture =
data.anm2.spritesheets.at(data.alternateSpritesheet.id).texture;
break;
case ALTERNATE:
spritesheets.at(data.alternateSpritesheet.id).texture = data.alternateSpritesheet.texture;
break;
default:
break;
}
spritesheetType = type;
}
}

93
src/entity/character.hpp Normal file
View File

@@ -0,0 +1,93 @@
#pragma once
#include "../resource/xml/character.hpp"
#include "../util/measurement.hpp"
#include "actor.hpp"
namespace game::entity
{
class Character : public Actor
{
public:
static constexpr auto DIGESTION_MAX = 100.0f;
enum SpritesheetType
{
NORMAL,
ALTERNATE
};
resource::xml::Character data;
float weight{};
int stage{0};
float calories{};
float capacity{};
float digestionProgress{};
float digestionRate{0.05f};
int digestionTimer{};
bool isDigesting{};
bool isJustDigested{};
float eatSpeed{1.0f};
float totalCaloriesConsumed{};
int totalFoodItemsEaten{};
int talkOverrideID{};
float talkTimer{};
bool isTalking{};
std::vector<int> animationTalkDurations{};
int blinkOverrideID{};
float blinkTimer{};
bool isBlinking{};
std::vector<int> animationBlinkDurations{};
bool isStageUp{};
bool isStageUpDuring{};
bool isJustStageUp{};
bool isJustStageFinal{};
SpritesheetType spritesheetType{};
std::map<int, Override> interactAreaOverrides{};
std::unordered_map<int, int> expandAreaOverrideLayerIDs{};
std::unordered_map<int, int> expandAreaOverrideNullIDs{};
Character() = default;
Character(const Character&);
Character(Character&&) noexcept;
Character& operator=(const Character&);
Character& operator=(Character&&) noexcept;
Character(resource::xml::Character&, glm::ivec2);
float weight_get(util::measurement::System = util::measurement::METRIC);
float digestion_rate_get();
float capacity_percent_get() const;
float max_capacity() const;
bool is_over_capacity() const;
bool is_max_capacity() const;
int stage_get() const;
int stage_from_weight_get(float weight) const;
int stage_max_get() const;
float stage_progress_get();
float stage_threshold_get(int stage = -1, util::measurement::System = util::measurement::METRIC) const;
float stage_threshold_next_get(util::measurement::System = util::measurement::METRIC) const;
void expand_areas_apply();
void spritesheet_set(SpritesheetType);
void update();
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_name_convert(const std::string& name);
};
}

22
src/entity/cursor.cpp Normal file
View File

@@ -0,0 +1,22 @@
#include "cursor.hpp"
#include "../util/imgui.hpp"
using namespace game::util;
using namespace glm;
namespace game::entity
{
Cursor::Cursor(Anm2& anm2) : Actor(anm2, imgui::to_vec2(ImGui::GetMousePos())) {}
void Cursor::tick()
{
Actor::tick();
queue_default_animation();
}
void Cursor::update()
{
state = DEFAULT;
position = imgui::to_vec2(ImGui::GetMousePos());
}
}

25
src/entity/cursor.hpp Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
#include "actor.hpp"
namespace game::entity
{
class Cursor : public Actor
{
public:
enum State
{
DEFAULT,
HOVER,
ACTION
};
State state{DEFAULT};
int interactTypeID{-1};
Cursor() = default;
Cursor(resource::xml::Anm2&);
void tick();
void update();
};
}

31
src/entity/item.cpp Normal file
View File

@@ -0,0 +1,31 @@
#include "item.hpp"
#include <imgui.h>
#include "../util/vector.hpp"
using game::resource::xml::Anm2;
using namespace game::util;
using namespace glm;
namespace game::entity
{
Item::Item(Anm2 _anm2, glm::ivec2 _position, int _schemaID, int _chewCount, int _animationIndex, glm::vec2 _velocity,
float _rotation)
: Actor(_anm2, _position, SET, 0.0f, _animationIndex), schemaID(_schemaID), chewCount(_chewCount),
velocity(_velocity)
{
rotationOverrideID =
vector::push_index(overrides, Override(-1, Anm2::ROOT, Override::SET, Anm2::FrameOptional{.rotation = _rotation}));
}
void Item::update()
{
auto& rotationOverride = overrides[rotationOverrideID];
position += velocity;
*rotationOverride.frame.rotation += angularVelocity;
}
}

26
src/entity/item.hpp Normal file
View File

@@ -0,0 +1,26 @@
#pragma once
#include "../resource/xml/item.hpp"
#include "actor.hpp"
namespace game::entity
{
class Item : public Actor
{
public:
bool isToBeDeleted{};
bool isHeld{};
int schemaID{};
int rotationOverrideID{};
int chewCount{};
glm::vec2 velocity{};
float angularVelocity{};
Item(resource::xml::Anm2, glm::ivec2 position, int id, int chewCount = 0, int animationIndex = -1,
glm::vec2 velocity = {}, float rotation = 0.0f);
void update();
};
}

View File

@@ -1,12 +0,0 @@
#pragma once
#include "types.h"
namespace game
{
class GameData
{
public:
MeasurementSystem measurementSystem{MeasurementSystem::METRIC};
};
}

View File

@@ -1,129 +0,0 @@
#include "item.h"
#include "imgui.h"
#include "util/math_.h"
using namespace game::anm2;
using namespace game::util;
using namespace glm;
namespace game
{
Item* Item::heldItem = nullptr;
Item* Item::heldItemPrevious = nullptr;
Item* Item::hoveredItem = nullptr;
Item* Item::hoveredItemPrevious = nullptr;
Item* Item::queuedReturnItem = nullptr;
std::array<Item::Pool, Item::RARITY_COUNT> rarity_pools_get()
{
std::array<Item::Pool, Item::RARITY_COUNT> newPools{};
for (auto& pool : newPools)
pool.clear();
for (int i = 0; i < Item::ITEM_COUNT; i++)
{
auto& rarity = Item::RARITIES[i];
newPools[rarity].emplace_back((Item::Type)i);
}
return newPools;
}
const std::array<Item::Pool, Item::RARITY_COUNT> Item::pools = rarity_pools_get();
Item::Item(Anm2* _anm2, glm::ivec2 _position, Type _type) : Actor(_anm2, _position, SET, (float)_type)
{
this->type = _type;
}
void Item::tick() { Actor::tick(); }
void Item::update(Resources& resources)
{
auto bounds = ivec4(position.x - SIZE * 0.5f, position.y - SIZE * 0.5f, SIZE, SIZE);
auto mousePos = ivec2(ImGui::GetMousePos().x, ImGui::GetMousePos().y);
if (isHeld)
{
position = mousePos - ivec2(holdOffset);
delta = previousPosition - position;
velocity *= VELOCITY_HOLD_MULTIPLIER;
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left))
{
auto power = fabs(delta.x) + fabs(delta.y);
heldItem = nullptr;
if (power > THROW_THRESHOLD)
resources.sound_play(audio::THROW);
else
resources.sound_play(audio::RELEASE);
velocity += delta;
isHeld = false;
}
}
else if (math::is_point_in_rect(bounds, mousePos))
{
hoveredItem = this;
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !heldItem)
{
heldItem = this;
resources.sound_play(audio::GRAB);
isHeld = true;
holdOffset = mousePos - ivec2(position);
}
else if (ImGui::IsMouseClicked(ImGuiMouseButton_Right))
{
queuedReturnItem = this;
}
}
if (!isHeld) velocity.y += GRAVITY;
position += velocity;
if (position.x < BOUNDS.x || position.x > BOUNDS.z)
{
velocity.x = -velocity.x;
velocity.x *= FRICTION;
if (fabs(velocity.x) > BOUNCE_SOUND_THRESHOLD) resources.sound_play(audio::BOUNCE);
}
if (position.y < BOUNDS.y || position.y > BOUNDS.w)
{
velocity.y = -velocity.y;
velocity *= FRICTION;
if (fabs(velocity.y) > BOUNCE_SOUND_THRESHOLD) resources.sound_play(audio::BOUNCE);
}
position = glm::clamp(vec2(BOUNDS.x, BOUNDS.y), position, vec2(BOUNDS.z, BOUNDS.w));
previousPosition = position;
}
void Item::state_set(State state)
{
this->state = state;
switch (this->state)
{
case DEFAULT:
play("Default", SET, (float)type);
break;
case CHEW_1:
play("Chew1", SET, (float)type);
break;
case CHEW_2:
play("Chew2", SET, (float)type);
break;
default:
break;
};
}
}

View File

@@ -1,241 +0,0 @@
#pragma once
#include "resource/actor.h"
#include "resources.h"
namespace game
{
class Item : public resource::Actor
{
public:
static constexpr auto VELOCITY_HOLD_MULTIPLIER = 0.50f;
static constexpr auto VELOCITY_HOLD_BOOST = 2.5f;
static constexpr auto BOUNCE_SOUND_THRESHOLD = 2.5f;
static constexpr auto THROW_MULTIPLIER = 1.0f;
static constexpr auto THROW_THRESHOLD = 50.0f;
static constexpr auto FRICTION = 0.75f;
static constexpr auto GRAVITY = 0.50f;
static constexpr auto SIZE = 72.0f;
static constexpr auto CHEW_COUNT_MAX = 2;
static constexpr auto DEPLOYED_MAX = 10;
static constexpr auto ANIMATION_STATE = "State";
static constexpr glm::vec4 BOUNDS = {50, 100, 475, 500};
static constexpr auto SPAWN_X_MIN = BOUNDS.x;
static constexpr auto SPAWN_X_MAX = BOUNDS.z + BOUNDS.x;
static constexpr auto SPAWN_Y_MIN = BOUNDS.y;
static constexpr auto SPAWN_Y_MAX = BOUNDS.w + BOUNDS.y;
#define CATEGORIES \
X(INVALID, "Invalid") \
X(FOOD, "Food") \
X(UTILITY, "Utility")
enum Category
{
#define X(symbol, name) symbol,
CATEGORIES
#undef X
};
static constexpr const char* CATEGORY_NAMES[] = {
#define X(symbol, name) name,
CATEGORIES
#undef X
};
#undef CATEGORIES
#define RARITIES \
X(NO_RARITY, "No Rarity", 0.0f) \
X(COMMON, "Common", 1.0f) \
X(UNCOMMON, "Uncommon", 0.75f) \
X(RARE, "Rare", 0.5f) \
X(EPIC, "Epic", 0.25f) \
X(LEGENDARY, "Legendary", 0.125f) \
X(IMPOSSIBLE, "???", 0.001f) \
X(SPECIAL, "Special", 0.0f)
enum Rarity
{
#define X(symbol, name, chance) symbol,
RARITIES
#undef X
RARITY_COUNT
};
static constexpr const char* RARITY_NAMES[] = {
#define X(symbol, name, chance) name,
RARITIES
#undef X
};
static constexpr float RARITY_CHANCES[] = {
#define X(symbol, name, chance) chance,
RARITIES
#undef X
};
#undef RARITIES
#define FLAVORS \
X(FLAVORLESS, "None") \
X(SWEET, "Sweet") \
X(BITTER, "Bitter") \
X(SPICY, "Spicy") \
X(MINT, "Mint") \
X(CITRUS, "Citrus") \
X(MOCHA, "Mocha") \
X(SPICE, "Spice")
enum Flavor
{
#define X(symbol, name) symbol,
FLAVORS
#undef X
FLAVOR_COUNT
};
static constexpr const char* FLAVOR_NAMES[] = {
#define X(symbol, name) name,
FLAVORS
#undef X
};
#undef FLAVORS
// clang-format off
#define ITEMS \
X(NONE, INVALID, FLAVORLESS, NO_RARITY, "", "", 0, 0, 0) \
X(POKE_PUFF_BASIC_SWEET, FOOD, SWEET, COMMON, "Poké Puff (Basic, Sweet)", "A basic sweet Poké Puff.", 100.0f, 0, 0) \
X(POKE_PUFF_BASIC_MINT, FOOD, MINT, COMMON, "Poké Puff (Basic, Mint)", "A basic minty Poké Puff.", 100.0f, 0, 0) \
X(POKE_PUFF_BASIC_CITRUS, FOOD, CITRUS, COMMON, "Poké Puff (Basic, Citrus)", "A basic citrusy Poké Puff.", 100.0f, 0, 0) \
X(POKE_PUFF_BASIC_MOCHA, FOOD, MOCHA, COMMON, "Poké Puff (Basic, Mocha)", "A basic mocha Poké Puff.", 100.0f, 0, 0) \
X(POKE_PUFF_BASIC_SPICE, FOOD, SPICE, COMMON, "Poké Puff (Basic, Spice)", "A basic spice Poké Puff.", 100.0f, 0, 0) \
X(POKE_PUFF_FROSTED_SWEET, FOOD, SWEET, UNCOMMON, "Poké Puff (Frosted, Sweet)", "A frosted sweet Poké Puff.", 250.0f, 0, 0) \
X(POKE_PUFF_FROSTED_MINT, FOOD, MINT, UNCOMMON, "Poké Puff (Frosted, Mint)", "A frosted minty Poké Puff.", 250.0f, 0, 0) \
X(POKE_PUFF_FROSTED_CITRUS, FOOD, MINT, UNCOMMON, "Poké Puff (Frosted, Citrus)", "A frosted citrusy Poké Puff.", 250.0f, 0, 0) \
X(POKE_PUFF_FROSTED_MOCHA, FOOD, MOCHA, UNCOMMON, "Poké Puff (Frosted, Mocha)", "A frosted mocha Poké Puff.", 250.0f, 0, 0) \
X(POKE_PUFF_FROSTED_SPICE, FOOD, SPICE, UNCOMMON, "Poké Puff (Frosted, Spice)", "A frosted spice Poké Puff.", 250.0f, 0, 0) \
X(POKE_PUFF_FANCY_SWEET, FOOD, SWEET, RARE, "Poké Puff (Fancy, Sweet)", "A fancy sweet Poké Puff, adorned with a cherry.", 500.0f, 0, 0) \
X(POKE_PUFF_FANCY_MINT, FOOD, MINT, RARE, "Poké Puff (Fancy, Mint)", "A fancy minty Poké Puff, adorned with a crescent moon-shaped candy.", 500.0f, 0, 0) \
X(POKE_PUFF_FANCY_CITRUS, FOOD, CITRUS, RARE, "Poké Puff (Fancy, Citrus)", "A fancy citrus Poké Puff, adorned with an orange slice.", 500.0f, 0, 0) \
X(POKE_PUFF_FANCY_MOCHA, FOOD, MOCHA, RARE, "Poké Puff (Fancy, Mocha)", "A fancy mocha Poké Puff, adorned with a morsel of white chocolate.", 500.0f, 0, 0) \
X(POKE_PUFF_FANCY_SPICE, FOOD, SPICE, RARE, "Poké Puff (Fancy, Spice)", "A fancy spice Poké Puff, adorned with a morsel of dark chocolate.", 500.0f, 0, 0) \
X(POKE_PUFF_DELUXE_SWEET, FOOD, SWEET, EPIC, "Poké Puff (Deluxe, Sweet)", "A deluxe sweet Poké Puff; frosted and adorned with a cherry.", 1000.0f, 0, 0) \
X(POKE_PUFF_DELUXE_MINT, FOOD, MINT, EPIC, "Poké Puff (Deluxe, Mint)", "A deluxe minty Poké Puff; frosted and adorned with a crescent moon-shapedd candy.", 1000.0f, 0, 0) \
X(POKE_PUFF_DELUXE_CITRUS, FOOD, CITRUS, EPIC, "Poké Puff (Deluxe, Citrus)", "A deluxe citrusy Poké Puff; frosted and adorned with an orange slice.", 1000.0f, 0, 0) \
X(POKE_PUFF_DELUXE_MOCHA, FOOD, MOCHA, EPIC, "Poké Puff (Deluxe, Mocha)", "A deluxe mocha Poké Puff; frosted and adorned with a morsel of white chocolate.", 1000.0f, 0, 0) \
X(POKE_PUFF_DELUXE_SPICE, FOOD, SPICE, EPIC, "Poké Puff (Deluxe, Spice)", "A deluxe spice Poké Puff; frosted and adorned with a morsel of dark chocolate.", 1000.0f, 0, 0) \
X(POKE_PUFF_SUPREME_SPRING, FOOD, SWEET, LEGENDARY, "Poké Puff (Supreme Spring)", "A supreme Poké Puff that tastes like the sweet cherry blossoms of spring.", 2500.0f, 0, 0) \
X(POKE_PUFF_SUPREME_SUMMER, FOOD, CITRUS, LEGENDARY, "Poké Puff (Supreme Summer)", "A supreme Poké Puff that tastes like a tropical summer vacation.", 2500.0f, 0, 0) \
X(POKE_PUFF_SUPREME_AUTUMN, FOOD, CITRUS, LEGENDARY, "Poké Puff (Supreme Autumn)", "A supreme Poké Puff that tastes like a bountiful autumn harvest.", 2500.0f, 0, 0) \
X(POKE_PUFF_SUPREME_WINTER, FOOD, SPICE, LEGENDARY, "Poké Puff (Supreme Winter)", "A supreme Poké Puff that tastes like a frosty winter wonderland.", 2500.0f, 0, 0) \
X(POKE_PUFF_SUPREME_WISH, FOOD, SPICE, IMPOSSIBLE, "Poké Puff (Supreme Wish)", "A supreme Poké Puff that tastes like a cherished birthday celebration.\nIt also is the biggest calorie nuke ever.\nCan Snivy eat it!?\nAlso, statistically, it might be your birthday today, right?", 10000.0f, 0, 0) \
X(POKE_PUFF_SUPREME_HONOR, FOOD, MOCHA, SPECIAL, "Poké Puff (Supreme Honor)", "A supreme Poké Puff that tastes like a monumental victory.\nAwarded for achieving a superb Play score.\nYou've earned it...if Snivy can eat it!", 2500.0f, 0.025f, 1.0f) \
X(BERRY_CHERI, FOOD, SPICY, UNCOMMON, "Cheri Berry", "A spherical red berry that cures paralysis\n...oh, and it also excites one's stomach.", 25.0f, 0.000833f, 0) \
X(BERRY_TAMATO, FOOD, SPICY, RARE, "Tamato Berry", "A spiny crimson berry that lowers speed and raises friendship.\n...oh, and it also greatly excites one's stomach.", 50.0f, 0.0025f, 0) \
X(BERRY_TOUGA, FOOD, SPICY, EPIC, "Touga Berry", "An obscure, searing-red berry from faraway lands.\nIt cures confusion...and it'll make one's stomach a burning inferno.", 100.0f, 0.00833f, 0) \
X(BERRY_PECHA, FOOD, SWEET, UNCOMMON, "Pecha Berry", "A plump and juicy pink berry that cures poison.\n...oh, and its sugars will stir appetite.", 25.0f, 0, 0.05f) \
X(BERRY_MAGOST, FOOD, SWEET, RARE, "Magost Berry", "A spherical, shining berry that packs a punch with a fibrious inside.\n...oh, and its sugars will greatly stir appetite.", 50.0f, 0, 0.15f) \
X(BERRY_WATMEL, FOOD, SWEET, EPIC, "Watmel Berry", "A truly bountiful berry with a brilliant green and pink pattern.\nWhispering tales say that if one eats such a berry,\ntheir appetite will increase to a point that they will soon bare the berry's shape.", 100.0f, 0, 0.50f) \
X(BERRY_AGUAV, FOOD, BITTER, RARE, "Aguav Berry", "A bitter berry that will restore health for hurt eaters who enjoy the flavor.\n...oh, also its taste will soothe an upset stomach.\n(Only use this if you really want to decrease Snivy's digestion.)", 50.0f, -0.01666f, 0) \
X(BERRY_POMEG, FOOD, FLAVORLESS, IMPOSSIBLE, "Pomeg Berry", "cG9tZWcgYmVycnkgcG9tZWcgYmVycnkgcG9tZWcgYmVycnk=", -2500.0f, 0.2, 2.0f)
// clang-format on
enum Type
{
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) symbol,
ITEMS
#undef X
ITEM_COUNT
};
static constexpr Category CATEGORIES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) category,
ITEMS
#undef X
};
static constexpr Flavor FLAVORS[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) flavor,
ITEMS
#undef X
};
static constexpr Rarity RARITIES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) rarity,
ITEMS
#undef X
};
static constexpr const char* NAMES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) name,
ITEMS
#undef X
};
static constexpr const char* DESCRIPTIONS[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) description,
ITEMS
#undef X
};
static constexpr float CALORIES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) calories,
ITEMS
#undef X
};
static constexpr float DIGESTION_RATE_BONUSES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) \
digestionRateBonus,
ITEMS
#undef X
};
static constexpr float EAT_SPEED_BONUSES[] = {
#define X(symbol, category, flavor, rarity, name, description, calories, digestionRateBonus, eatSpeedBonus) \
eatSpeedBonus,
ITEMS
#undef X
};
#undef ITEMS
enum State
{
DEFAULT,
CHEW_1,
CHEW_2
};
static Item* heldItem;
static Item* heldItemPrevious;
static Item* hoveredItem;
static Item* hoveredItemPrevious;
static Item* queuedReturnItem;
using Pool = std::vector<Type>;
static const std::array<Pool, RARITY_COUNT> pools;
Type type{NONE};
State state{DEFAULT};
int chewCount{0};
bool isToBeDeleted{};
glm::vec2 delta{};
glm::vec2 previousPosition{};
glm::vec2 velocity{};
glm::vec2 holdOffset{};
bool isHeld{};
Item(anm2::Anm2*, glm::ivec2, Type);
void state_set(State);
void tick();
void update(Resources& resources);
};
}

View File

@@ -1,4 +1,5 @@
#include "loader.h"
#include "loader.hpp"
#include "util/imgui/widget.hpp"
#ifdef __EMSCRIPTEN__
#include <GLES3/gl3.h>
@@ -8,45 +9,70 @@
#include <backends/imgui_impl_opengl3.h>
#include <backends/imgui_impl_sdl3.h>
#include <format>
#include <imgui.h>
#include <iostream>
#include "util/math_.h"
#include "log.hpp"
#include "util/math.hpp"
#include <SDL3_mixer/SDL_mixer.h>
#include <physfs.h>
#include "resource/audio.hpp"
#include "resource/xml/settings.hpp"
#include "util/imgui/style.hpp"
#include "util/preferences.hpp"
#ifdef __EMSCRIPTEN__
#include "util/web_filesystem.hpp"
constexpr auto GL_VERSION_MAJOR = 3;
constexpr auto GL_VERSION_MINOR = 0;
constexpr auto GLSL_VERSION = "#version 300 es";
#else
constexpr auto GL_VERSION_MAJOR = 3;
constexpr auto GL_VERSION_MINOR = 3;
constexpr auto GLSL_VERSION = "#version 330";
#endif
constexpr auto WINDOW_ROUNDING = 6.0f;
constexpr auto WINDOW_COLOR = ImVec4(0.03f, 0.25f, 0.06f, 1.0f);
constexpr auto WINDOW_BACKGROUND_COLOR = ImVec4(0.02f, 0.08f, 0.03f, 0.96f);
constexpr auto ACCENT_COLOR = ImVec4(0.05f, 0.32f, 0.12f, 1.0f);
constexpr auto ACCENT_COLOR_HOVERED = ImVec4(0.07f, 0.4f, 0.15f, 1.0f);
constexpr auto ACCENT_COLOR_ACTIVE = ImVec4(0.09f, 0.5f, 0.2f, 1.0f);
constexpr auto TAB_UNFOCUSED_COLOR = ImVec4(0.03f, 0.2f, 0.07f, 0.9f);
using namespace game::util;
namespace game
{
Loader::Loader()
Loader::Loader(int argc, const char** argv)
{
if (!SDL_Init(SDL_INIT_VIDEO))
#ifdef __EMSCRIPTEN__
util::web_filesystem::init_and_wait();
#endif
settings = resource::xml::Settings(preferences::path() / "settings.xml");
logger.info("Initializing...");
if (!PHYSFS_init((argc > 0 && argv && argv[0]) ? argv[0] : "snivy"))
{
std::cout << "Failed to initialize SDL: " << SDL_GetError();
logger.fatal(std::format("Failed to initialize PhysicsFS: {}", PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())));
isError = true;
return;
}
std::cout << "Initialized SDL" << "\n";
PHYSFS_setWriteDir(nullptr);
logger.info("Initialized PhysFS");
if (!SDL_Init(SDL_INIT_VIDEO))
{
logger.fatal(std::format("Failed to initialize SDL: {}", SDL_GetError()));
isError = true;
return;
}
logger.info("Initialized SDL");
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, GL_VERSION_MAJOR);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, GL_VERSION_MINOR);
@@ -57,30 +83,50 @@ namespace game
#endif
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
window = SDL_CreateWindow("Snivy", SIZE.x, SIZE.y, SDL_WINDOW_OPENGL);
#ifdef __EMSCRIPTEN__
static constexpr glm::vec2 SIZE = {1600, 900};
window = SDL_CreateWindow("Feed Snivy", 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_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);
SDL_SetNumberProperty(windowProperties, SDL_PROP_WINDOW_CREATE_X_NUMBER,
settings.windowPosition.x == 0 ? SDL_WINDOWPOS_CENTERED : (long)settings.windowPosition.x);
SDL_SetNumberProperty(windowProperties, SDL_PROP_WINDOW_CREATE_Y_NUMBER,
settings.windowPosition.y == 0 ? SDL_WINDOWPOS_CENTERED : (long)settings.windowPosition.y);
SDL_SetBooleanProperty(windowProperties, SDL_PROP_WINDOW_CREATE_RESIZABLE_BOOLEAN, true);
SDL_SetBooleanProperty(windowProperties, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true);
SDL_SetBooleanProperty(windowProperties, SDL_PROP_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, true);
window = SDL_CreateWindowWithProperties(windowProperties);
#endif
if (!window)
{
std::cout << "Failed to initialize window: " << SDL_GetError();
;
logger.fatal(std::format("Failed to initialize window: {}", SDL_GetError()));
isError = true;
return;
}
std::cout << "Initialized window" << "\n";
logger.info("Initialized window");
context = SDL_GL_CreateContext(window);
if (!context)
{
std::cout << "Failed to initialize GL context: " << SDL_GetError();
logger.fatal(std::format("Failed to initialize GL context: {}", SDL_GetError()));
isError = true;
return;
}
if (!SDL_GL_MakeCurrent(window, context))
{
std::cout << "Failed to make GL context current: " << SDL_GetError();
logger.fatal(std::format("Failed to make GL context current: {}", SDL_GetError()));
isError = true;
return;
}
@@ -88,12 +134,12 @@ namespace game
#ifndef __EMSCRIPTEN__
if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress))
{
std::cout << "Failed to initialize GLAD" << "\n";
logger.fatal("Failed to initialize GLAD");
isError = true;
return;
}
std::cout << "Initialized GLAD" << "\n";
logger.info("Initialized GLAD");
#endif
glEnable(GL_BLEND);
@@ -101,90 +147,69 @@ namespace game
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_DEPTH_TEST);
std::cout << "Initialized GL context: " << glGetString(GL_VERSION) << "\n";
logger.info(std::format("Initialized GL context: {}", (const char*)glGetString(GL_VERSION)));
SDL_GL_SetSwapInterval(1);
SDL_GL_MakeCurrent(window, context);
if (!MIX_Init())
{
std::cout << "Failed to initialize SDL mixer: " << SDL_GetError();
logger.fatal(std::format("Failed to initialize SDL mixer: {}", SDL_GetError()));
isError = true;
return;
}
std::cout << "Initialized SDL mixer" << "\n";
logger.info("Initialized SDL mixer");
IMGUI_CHECKVERSION();
ImGuiContext* imguiContext = ImGui::CreateContext();
if (!imguiContext)
{
std::cout << "Failed to initialize Dear ImGui" << "\n";
logger.fatal("Failed to initialize Dear ImGui");
isError = true;
return;
}
std::cout << "Initialized Dear ImGui" << "\n";
logger.info("Initialized Dear ImGui");
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr;
io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
ImGuiStyle& style = ImGui::GetStyle();
ImGui::StyleColorsDark();
style.Colors[ImGuiCol_TitleBg] = WINDOW_COLOR;
style.Colors[ImGuiCol_TitleBgActive] = WINDOW_COLOR;
style.Colors[ImGuiCol_TitleBgCollapsed] = WINDOW_COLOR;
style.Colors[ImGuiCol_Header] = ACCENT_COLOR;
style.Colors[ImGuiCol_HeaderHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_HeaderActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_FrameBg] = ACCENT_COLOR;
style.Colors[ImGuiCol_FrameBgActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_FrameBgHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_Button] = ACCENT_COLOR;
style.Colors[ImGuiCol_ButtonHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_ButtonActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_CheckMark] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_SliderGrab] = ACCENT_COLOR;
style.Colors[ImGuiCol_SliderGrabActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_ResizeGrip] = ACCENT_COLOR;
style.Colors[ImGuiCol_ResizeGripHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_ResizeGripActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_PlotLines] = ACCENT_COLOR;
style.Colors[ImGuiCol_PlotLinesHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_PlotHistogram] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_PlotHistogramHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_Tab] = ACCENT_COLOR;
style.Colors[ImGuiCol_TabHovered] = ACCENT_COLOR_HOVERED;
style.Colors[ImGuiCol_TabActive] = ACCENT_COLOR_ACTIVE;
style.Colors[ImGuiCol_TabUnfocused] = TAB_UNFOCUSED_COLOR;
style.Colors[ImGuiCol_TabUnfocusedActive] = ACCENT_COLOR;
if (!ImGui_ImplSDL3_InitForOpenGL(window, context))
{
std::cout << "Failed to initialize Dear ImGui SDL3 backend" << "\n";
logger.fatal("Failed to initialize Dear ImGui SDL3 backend");
ImGui::DestroyContext(imguiContext);
isError = true;
return;
}
std::cout << "Initialize Dear ImGui SDL3 backend" << "\n";
logger.info("Initialized Dear ImGui SDL3 backend");
if (!ImGui_ImplOpenGL3_Init(GLSL_VERSION))
{
std::cout << "Failed to initialize Dear ImGui OpenGL backend" << "\n";
logger.fatal("Failed to initialize Dear ImGui OpenGL backend");
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(imguiContext);
isError = true;
return;
}
std::cout << "Initialize Dear ImGui OpenGL backend" << "\n";
logger.info("Initialized Dear ImGui OpenGL backend");
imgui::style::color_set(settings.color);
imgui::style::rounding_set();
math::random_seed_set();
resource::Audio::volume_set((float)settings.volume / 100);
}
Loader::~Loader()
{
PHYSFS_deinit();
if (ImGui::GetCurrentContext())
{
ImGui_ImplOpenGL3_Shutdown();
@@ -196,6 +221,6 @@ namespace game
if (window) SDL_DestroyWindow(window);
SDL_Quit();
std::cout << "Exiting..." << "\n";
logger.info("Exiting...");
}
};

View File

@@ -1,20 +1,20 @@
#pragma once
#include "glm/ext/vector_float2.hpp"
#include "resource/xml/settings.hpp"
#include <SDL3/SDL.h>
#include <glm/ext/vector_float2.hpp>
namespace game
{
class Loader
{
public:
static constexpr glm::vec2 SIZE = {1080, 720};
SDL_Window* window{};
SDL_GLContext context{};
bool isError{};
resource::xml::Settings settings;
Loader();
Loader(int argc, const char** argv);
~Loader();
};

111
src/log.cpp Normal file
View File

@@ -0,0 +1,111 @@
#include "log.hpp"
#include <cstdio>
#include <format>
#include <exception>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <sstream>
#include <streambuf>
#include <thread>
#include "util/preferences.hpp"
#include "util/time.hpp"
using namespace game::util;
namespace game
{
class StderrToLoggerBuf final : public std::streambuf
{
Logger* logger{};
std::string buffer;
std::mutex mutex;
public:
explicit StderrToLoggerBuf(Logger* target) : logger(target) {}
protected:
int overflow(int ch) override
{
if (ch == traits_type::eof()) return traits_type::not_eof(ch);
std::lock_guard<std::mutex> lock(mutex);
if (ch == '\n')
{
if (logger && !buffer.empty())
{
logger->error(buffer);
buffer.clear();
}
}
else
{
buffer.push_back(static_cast<char>(ch));
}
return ch;
}
int sync() override
{
std::lock_guard<std::mutex> lock(mutex);
if (logger && !buffer.empty())
{
logger->error(buffer);
buffer.clear();
}
return 0;
}
};
std::streambuf* old_stderr_buf = nullptr;
void Logger::write_raw(const std::string& message)
{
std::fwrite(message.c_str(), 1, message.size(), stdout);
std::fwrite("\n", 1, 1, stdout);
std::fflush(stdout);
if (file.is_open()) file << message << '\n' << std::flush;
}
void Logger::write(const Level level, const std::string& message)
{
std::string formatted = std::format("{} {} {}", LEVEL_STRINGS[level], time::get("(%d-%B-%Y %I:%M:%S)"), message);
write_raw(formatted);
}
void Logger::info(const std::string& message) { write(LEVEL_INFO, message); }
void Logger::warning(const std::string& message) { write(LEVEL_WARNING, message); }
void Logger::error(const std::string& message) { write(LEVEL_ERROR, message); }
void Logger::fatal(const std::string& message) { write(LEVEL_FATAL, message); }
void Logger::open(const std::filesystem::path& path) { file.open(path, std::ios::out | std::ios::app); }
std::filesystem::path Logger::path() { return preferences::path() / "log.txt"; }
Logger::Logger()
{
open(path());
static StderrToLoggerBuf stderr_buf(this);
old_stderr_buf = std::cerr.rdbuf(&stderr_buf);
std::cerr.setf(std::ios::unitbuf);
std::set_terminate(
[]
{
try
{
if (auto eptr = std::current_exception()) std::rethrow_exception(eptr);
}
catch (const std::exception& ex)
{
logger.fatal(std::string("Unhandled exception: ") + ex.what());
}
catch (...)
{
logger.fatal("Unhandled exception: <unknown>");
}
std::abort();
});
}
}
game::Logger logger;

48
src/log.hpp Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
#include <filesystem>
#include <fstream>
#include <string>
#include <string_view>
namespace game
{
#define LEVELS \
X(LEVEL_INFO, "[INFO]") \
X(LEVEL_WARNING, "[WARNING]") \
X(LEVEL_ERROR, "[ERROR]") \
X(LEVEL_FATAL, "[FATAL]")
enum Level
{
#define X(symbol, string) symbol,
LEVELS
#undef X
};
constexpr std::string_view LEVEL_STRINGS[] = {
#define X(symbol, string) string,
LEVELS
#undef X
};
#undef LEVELS
class Logger
{
std::ofstream file{};
public:
static std::filesystem::path path();
void write_raw(const std::string&);
void write(const Level, const std::string&);
void info(const std::string&);
void warning(const std::string&);
void error(const std::string&);
void fatal(const std::string&);
void open(const std::filesystem::path&);
Logger();
};
}
extern game::Logger logger;

View File

@@ -4,8 +4,8 @@
#include <emscripten/emscripten.h>
#endif
#include "loader.h"
#include "state.h"
#include "loader.hpp"
#include "state.hpp"
using namespace game;
@@ -19,13 +19,13 @@ static void emscripten_loop(void* arg)
}
#endif
int main()
int main(int argc, const char** argv)
{
Loader loader;
Loader loader(argc, argv);
if (loader.isError) return EXIT_FAILURE;
State state(loader.window, loader.context, Loader::SIZE);
State state(loader.window, loader.context, loader.settings);
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(emscripten_loop, &state, 0, true);

View File

@@ -1,8 +1,14 @@
#include "canvas.h"
#include "canvas.hpp"
#include <glm/gtc/type_ptr.hpp>
#include <utility>
#include "../util/imgui.hpp"
#include "../util/math.hpp"
using namespace glm;
using namespace game::resource;
using namespace game::util;
namespace game
{
@@ -13,16 +19,16 @@ namespace game
GLuint Canvas::rectVBO = 0;
bool Canvas::isStaticInit = false;
Canvas::Canvas(vec2 size, bool isDefault)
Canvas::Canvas(ivec2 size, Flags flags)
{
this->size = size;
this->flags = flags;
if (isDefault)
if ((flags & DEFAULT) != 0)
{
fbo = 0;
rbo = 0;
texture = 0;
this->isDefault = true;
}
else
{
@@ -69,7 +75,6 @@ namespace game
glBindVertexArray(0);
// Rect
glGenVertexArrays(1, &rectVAO);
glGenBuffers(1, &rectVBO);
@@ -82,12 +87,47 @@ namespace game
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindVertexArray(0);
isStaticInit = true;
}
}
Canvas::Canvas(const Canvas& other) : Canvas(other.size, other.flags)
{
pan = other.pan;
zoom = other.zoom;
if ((flags & DEFAULT) == 0 && (other.flags & DEFAULT) == 0)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, other.fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
glBlitFramebuffer(0, 0, other.size.x, other.size.y, 0, 0, size.x, size.y, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
}
Canvas::Canvas(Canvas&& other) noexcept
{
size = other.size;
pan = other.pan;
zoom = other.zoom;
flags = other.flags;
fbo = other.fbo;
rbo = other.rbo;
texture = other.texture;
other.size = {};
other.pan = {};
other.zoom = 100.0f;
other.flags = FLIP;
other.fbo = 0;
other.rbo = 0;
other.texture = 0;
}
Canvas::~Canvas()
{
if (!isDefault)
if ((flags & DEFAULT) == 0)
{
if (fbo) glDeleteFramebuffers(1, &fbo);
if (rbo) glDeleteRenderbuffers(1, &rbo);
@@ -95,15 +135,66 @@ namespace game
}
}
mat4 Canvas::view_get() const { return mat4{1.0f}; }
mat4 Canvas::projection_get() const
Canvas& Canvas::operator=(const Canvas& other)
{
if (isDefault) return glm::ortho(0.0f, (float)size.x, (float)size.y, 0.0f, -1.0f, 1.0f);
return glm::ortho(0.0f, (float)size.x, 0.0f, (float)size.y, -1.0f, 1.0f);
if (this == &other) return *this;
Canvas tmp(other);
*this = std::move(tmp);
return *this;
}
void Canvas::texture_render(Shader& shader, GLuint textureId, mat4& model, vec4 tint, vec3 colorOffset,
float* vertices) const
Canvas& Canvas::operator=(Canvas&& other) noexcept
{
if (this == &other) return *this;
if ((flags & DEFAULT) == 0)
{
if (fbo) glDeleteFramebuffers(1, &fbo);
if (rbo) glDeleteRenderbuffers(1, &rbo);
if (texture) glDeleteTextures(1, &texture);
}
size = other.size;
pan = other.pan;
zoom = other.zoom;
flags = other.flags;
fbo = other.fbo;
rbo = other.rbo;
texture = other.texture;
other.size = {};
other.pan = {};
other.zoom = 100.0f;
other.flags = FLIP;
other.fbo = 0;
other.rbo = 0;
other.texture = 0;
return *this;
}
mat4 Canvas::view_get() const
{
auto view = mat4{1.0f};
auto zoomFactor = math::to_unit(zoom);
auto panFactor = pan * zoomFactor;
view = glm::translate(view, vec3(-panFactor, 0.0f));
view = glm::scale(view, vec3(zoomFactor, zoomFactor, 1.0f));
return view;
}
mat4 Canvas::projection_get() const
{
if ((flags & FLIP) != 0)
{
return glm::ortho(0.0f, (float)size.x, 0.0f, (float)size.y, -1.0f, 1.0f);
}
return glm::ortho(0.0f, (float)size.x, (float)size.y, 0.0f, -1.0f, 1.0f);
}
void Canvas::texture_render(Shader& shader, GLuint textureID, mat4 model, vec4 tint, vec3 colorOffset,
const float* vertices) const
{
glUseProgram(shader.id);
@@ -122,7 +213,7 @@ namespace game
glBufferData(GL_ARRAY_BUFFER, sizeof(TEXTURE_VERTICES), vertices, GL_DYNAMIC_DRAW);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureId);
glBindTexture(GL_TEXTURE_2D, textureID);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
@@ -156,14 +247,30 @@ namespace game
texture_render(shader, texture, model, tint, colorOffset);
}
void Canvas::bind() const
void Canvas::bind()
{
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glViewport(0, 0, size.x, size.y);
}
void Canvas::clear(vec4 color) const
void Canvas::size_set(ivec2 newSize)
{
if ((flags & DEFAULT) == 0 && (newSize.x != this->size.x || newSize.y != this->size.y))
{
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newSize.x, newSize.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glBindTexture(GL_TEXTURE_2D, 0);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, newSize.x, newSize.y);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
}
this->size = newSize;
glViewport(0, 0, newSize.x, newSize.y);
}
void Canvas::clear(vec4 color)
{
glClearColor(color.r, color.g, color.b, color.a);
glClear(GL_COLOR_BUFFER_BIT);
@@ -175,5 +282,14 @@ namespace game
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
bool Canvas::is_valid() const { return fbo != 0 || isDefault; };
bool Canvas::is_valid() const { return (flags & DEFAULT) != 0 || fbo != 0; };
glm::vec2 Canvas::screen_position_convert(glm::vec2 position) const
{
auto viewport = ImGui::GetMainViewport();
auto viewportPos = imgui::to_vec2(viewport->Pos);
auto localPosition = position - viewportPos;
auto zoomFactor = math::to_unit(zoom);
return pan + (localPosition / zoomFactor);
}
}

View File

@@ -6,7 +6,7 @@
#include <glad/glad.h>
#endif
#include "resource/shader.h"
#include "../resource/shader.hpp"
#include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/glm.hpp>
@@ -30,27 +30,45 @@ namespace game
static bool isStaticInit;
public:
static constexpr glm::vec4 CLEAR_COLOR = {0, 0, 0, 0};
enum Flag
{
DEFAULT = (1 << 0),
FLIP = (1 << 1)
};
using Flags = int;
GLuint fbo{};
GLuint rbo{};
GLuint texture{};
glm::vec2 size{};
bool isDefault{};
glm::ivec2 size{};
glm::vec2 pan{};
float zoom{100.0f};
Flags flags{FLIP};
Canvas() = default;
Canvas(glm::vec2, bool isDefault = false);
Canvas(glm::ivec2, Flags = FLIP);
Canvas(const Canvas&);
Canvas(Canvas&&) noexcept;
~Canvas();
Canvas& operator=(const Canvas&);
Canvas& operator=(Canvas&&) noexcept;
glm::mat4 transform_get() const;
glm::mat4 view_get() const;
glm::mat4 projection_get() const;
void texture_render(resource::Shader&, GLuint, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {},
float* = (float*)TEXTURE_VERTICES) const;
void texture_render(resource::Shader&, GLuint, glm::mat4, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {},
const float* = TEXTURE_VERTICES) const;
void texture_render(resource::Shader&, const Canvas&, glm::mat4, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}) const;
void rect_render(resource::Shader&, glm::mat4&, glm::vec4 = glm::vec4(0, 0, 1, 1)) const;
void render(resource::Shader&, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}) const;
void bind() const;
void bind();
void size_set(glm::ivec2 size);
void clear(glm::vec4 color = CLEAR_COLOR);
void unbind() const;
void clear(glm::vec4 color = glm::vec4(0, 0, 0, 1)) const;
bool is_valid() const;
glm::vec2 screen_position_convert(glm::vec2 position) const;
};
}

View File

@@ -1,421 +0,0 @@
#include "actor.h"
#include "../util/map_.h"
#include "../util/math_.h"
#include "../util/unordered_map_.h"
#include "../util/vector_.h"
#include "../resource/audio.h"
#include "../resource/texture.h"
#include <glm/glm.hpp>
#include <iostream>
using namespace glm;
using namespace game::util;
using namespace game::anm2;
namespace game::resource
{
Actor::Actor(Anm2* _anm2, vec2 _position, Mode mode, float time) : anm2(_anm2), position(_position)
{
if (anm2)
{
this->mode = mode;
this->startTime = time;
play(anm2->animations.defaultAnimation, mode, time);
}
}
anm2::Animation* Actor::animation_get(int index)
{
if (!anm2) return nullptr;
if (index == -1) index = animationIndex;
if (anm2->animations.mapReverse.contains(index)) return &anm2->animations.items[index];
return nullptr;
}
anm2::Animation* Actor::animation_get(const std::string& name)
{
if (!anm2) return nullptr;
if (anm2->animations.map.contains(name)) return &anm2->animations.items[anm2->animations.map[name]];
return nullptr;
}
bool Actor::is_playing(const std::string& name)
{
if (!anm2) return false;
if (name.empty())
return isPlaying;
else
return isPlaying && anm2->animations.map[name] == animationIndex;
}
int Actor::animation_index_get(const std::string& name)
{
if (!anm2) return -1;
if (anm2->animations.map.contains(name)) return anm2->animations.map[name];
return -1;
}
int Actor::item_id_get(const std::string& name, anm2::Type type)
{
if (!anm2 || (type != anm2::LAYER && type != anm2::NULL_)) return -1;
if (type == anm2::LAYER)
{
for (int i = 0; i < anm2->content.layers.size(); i++)
if (anm2->content.layers.at(i).name == name) return i;
}
else if (type == anm2::NULL_)
{
for (int i = 0; i < anm2->content.nulls.size(); i++)
if (anm2->content.nulls.at(i).name == name) return i;
}
return -1;
}
anm2::Item* Actor::item_get(anm2::Type type, int id, int animationIndex)
{
if (!anm2) return nullptr;
if (animationIndex == -1) animationIndex = this->animationIndex;
if (auto animation = animation_get(animationIndex))
{
switch (type)
{
case anm2::ROOT:
return &animation->rootAnimation;
break;
case anm2::LAYER:
return unordered_map::find(animation->layerAnimations, id);
case anm2::NULL_:
return map::find(animation->nullAnimations, id);
break;
case anm2::TRIGGER:
return &animation->triggers;
default:
return nullptr;
}
}
return nullptr;
}
int Actor::item_length(anm2::Item* item)
{
if (!item) return -1;
int duration{};
for (auto& frame : item->frames)
duration += frame.duration;
return duration;
}
anm2::Frame* Actor::trigger_get(int atFrame)
{
if (auto item = item_get(anm2::TRIGGER))
for (auto& trigger : item->frames)
if (trigger.atFrame == atFrame) return &trigger;
return nullptr;
}
anm2::Frame* Actor::frame_get(int index, anm2::Type type, int id)
{
if (auto item = item_get(type, id)) return vector::find(item->frames, index);
return nullptr;
}
bool Actor::is_event(const std::string& event)
{
if (!anm2) return false;
if (playedEventID == -1) return false;
return event == anm2->content.events.at(playedEventID).name;
}
anm2::Frame Actor::frame_generate(anm2::Item& item, float time, anm2::Type type, int id)
{
anm2::Frame frame{};
frame.isVisible = false;
if (item.frames.empty()) return frame;
time = time < 0.0f ? 0.0f : time;
anm2::Frame* frameNext = nullptr;
anm2::Frame frameNextCopy{};
int durationCurrent = 0;
int durationNext = 0;
for (int i = 0; i < item.frames.size(); i++)
{
anm2::Frame& checkFrame = item.frames[i];
frame = checkFrame;
durationNext += frame.duration;
if (time >= durationCurrent && time < durationNext)
{
if (i + 1 < (int)item.frames.size())
{
frameNext = &item.frames[i + 1];
frameNextCopy = *frameNext;
}
else
frameNext = nullptr;
break;
}
durationCurrent += frame.duration;
}
for (auto& override : overrides)
{
if (!override || !override->isEnabled) continue;
if (id == override->destinationID)
{
switch (override->mode)
{
case Override::FRAME_ADD:
if (override->frame.scale.has_value())
{
frame.scale += *override->frame.scale;
if (frameNext) frameNextCopy.scale += *override->frame.scale;
}
if (override->frame.rotation.has_value())
{
frame.rotation += *override->frame.rotation;
if (frameNext) frameNextCopy.rotation += *override->frame.rotation;
}
break;
case Override::FRAME_SET:
if (override->frame.scale.has_value())
{
frame.scale = *override->frame.scale;
if (frameNext) frameNextCopy.scale = *override->frame.scale;
}
if (override->frame.rotation.has_value())
{
frame.rotation = *override->frame.rotation;
if (frameNext) frameNextCopy.rotation = *override->frame.rotation;
}
break;
case Override::ITEM_SET:
default:
if (override->animationIndex == -1) break;
auto& animation = anm2->animations.items[override->animationIndex];
auto overrideFrame = frame_generate(animation.layerAnimations[override->sourceID], override->time,
anm2::LAYER, override->sourceID);
frame.crop = overrideFrame.crop;
break;
}
}
}
if (frame.isInterpolated && frameNext && frame.duration > 1)
{
auto interpolation = (time - durationCurrent) / (durationNext - durationCurrent);
frame.rotation = glm::mix(frame.rotation, frameNextCopy.rotation, interpolation);
frame.position = glm::mix(frame.position, frameNextCopy.position, interpolation);
frame.scale = glm::mix(frame.scale, frameNextCopy.scale, interpolation);
frame.colorOffset = glm::mix(frame.colorOffset, frameNextCopy.colorOffset, interpolation);
frame.tint = glm::mix(frame.tint, frameNextCopy.tint, interpolation);
}
return frame;
}
void Actor::play(int index, Mode mode, float time, float speedMultiplier)
{
this->playedEventID = -1;
this->playedTriggers.clear();
if (!anm2) return;
if (mode != FORCE_PLAY && this->animationIndex == index) return;
if (!vector::in_bounds(anm2->animations.items, index)) return;
this->speedMultiplier = speedMultiplier;
this->previousAnimationIndex = animationIndex;
this->animationIndex = index;
this->time = time;
if (mode == PLAY || mode == FORCE_PLAY) isPlaying = true;
}
void Actor::play(const std::string& name, Mode mode, float time, float speedMultiplier)
{
if (!anm2) return;
if (anm2->animations.map.contains(name))
play(anm2->animations.map.at(name), mode, time, speedMultiplier);
else
std::cout << "Animation \"" << name << "\" does not exist! Unable to play!\n";
}
void Actor::tick()
{
if (!anm2) return;
if (!isPlaying) return;
auto animation = animation_get();
if (!animation) return;
playedEventID = -1;
for (auto& trigger : animation->triggers.frames)
{
if (!playedTriggers.contains(trigger.atFrame) && time >= trigger.atFrame)
{
if (auto sound = map::find(anm2->content.sounds, trigger.soundID)) sound->audio.play();
playedTriggers.insert((int)trigger.atFrame);
playedEventID = trigger.eventID;
}
}
auto increment = (anm2->info.fps / 30.0f) * speedMultiplier;
time += increment;
if (time >= animation->frameNum)
{
if (animation->isLoop)
time = 0.0f;
else
isPlaying = false;
playedTriggers.clear();
}
for (auto& override : overrides)
{
if (!override->isEnabled || override->length < 0) continue;
override->time += increment;
if (override->time > override->length) override->isLoop ? override->time = 0.0f : override->isEnabled = false;
}
}
glm::vec4 Actor::null_frame_rect(int nullID)
{
auto invalidRect = glm::vec4(0.0f / 0.0f);
if (!anm2 || nullID == -1) return invalidRect;
auto item = item_get(anm2::NULL_, nullID);
if (!item) return invalidRect;
auto animation = animation_get();
if (!animation) return invalidRect;
auto root = frame_generate(animation->rootAnimation, time, anm2::ROOT);
for (auto& override : overrides)
{
if (!override || !override->isEnabled || override->type != anm2::ROOT) continue;
switch (override->mode)
{
case Override::FRAME_ADD:
if (override->frame.scale.has_value()) root.scale += *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation += *override->frame.rotation;
break;
case Override::FRAME_SET:
if (override->frame.scale.has_value()) root.scale = *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation = *override->frame.rotation;
break;
default:
break;
}
}
auto frame = frame_generate(*item, time, anm2::NULL_, nullID);
if (!frame.isVisible) return invalidRect;
auto rootScale = math::to_unit(root.scale);
auto frameScale = math::to_unit(frame.scale);
auto combinedScale = rootScale * frameScale;
auto scaledSize = NULL_SIZE * glm::abs(combinedScale);
auto worldPosition = position + root.position + frame.position * rootScale;
auto halfSize = scaledSize * 0.5f;
return glm::vec4(worldPosition - halfSize, scaledSize);
}
void Actor::render(Shader& textureShader, Shader& rectShader, Canvas& canvas)
{
if (!anm2) return;
auto animation = animation_get();
if (!animation) return;
auto root = frame_generate(animation->rootAnimation, time, anm2::ROOT);
for (auto& override : overrides)
{
if (!override || !override->isEnabled || override->type != anm2::ROOT) continue;
switch (override->mode)
{
case Override::FRAME_ADD:
if (override->frame.scale.has_value()) root.scale += *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation += *override->frame.rotation;
break;
case Override::FRAME_SET:
if (override->frame.scale.has_value()) root.scale = *override->frame.scale;
if (override->frame.rotation.has_value()) root.rotation = *override->frame.rotation;
break;
default:
break;
}
}
auto rootModel =
math::quad_model_parent_get(root.position + position, root.pivot, math::to_unit(root.scale), root.rotation);
for (auto& i : animation->layerOrder)
{
auto& layerAnimation = animation->layerAnimations[i];
if (!layerAnimation.isVisible) continue;
auto layer = map::find(anm2->content.layers, i);
if (!layer) continue;
auto spritesheet = map::find(anm2->content.spritesheets, layer->spritesheetID);
if (!spritesheet) continue;
auto frame = frame_generate(layerAnimation, time, anm2::LAYER, i);
if (!frame.isVisible) continue;
auto model =
math::quad_model_get(frame.size, frame.position, frame.pivot, math::to_unit(frame.scale), frame.rotation);
model = rootModel * model;
auto& texture = spritesheet->texture;
if (!texture.is_valid()) return;
auto tint = frame.tint * root.tint;
auto colorOffset = frame.colorOffset + root.colorOffset;
auto uvMin = frame.crop / vec2(texture.size);
auto uvMax = (frame.crop + frame.size) / vec2(texture.size);
auto uvVertices = math::uv_vertices_get(uvMin, uvMax);
canvas.texture_render(textureShader, texture.id, model, tint, colorOffset, uvVertices.data());
}
if (isShowNulls)
{
for (int i = 0; i < animation->nullAnimations.size(); i++)
{
auto& nullAnimation = animation->nullAnimations[i];
if (!nullAnimation.isVisible) continue;
auto frame = frame_generate(nullAnimation, time, anm2::NULL_, i);
if (!frame.isVisible) continue;
auto model = math::quad_model_get(frame.scale, frame.position, frame.scale * 0.5f, vec2(1.0f), frame.rotation);
model = rootModel * model;
canvas.rect_render(rectShader, model);
}
}
}
void Actor::consume_played_event() { playedEventID = -1; }
};

View File

@@ -1,80 +0,0 @@
#pragma once
#include <unordered_set>
#include "../canvas.h"
#include "anm2.h"
namespace game::resource
{
class Actor
{
public:
static constexpr auto NULL_SIZE = glm::vec2(100, 100);
enum Mode
{
PLAY,
SET,
FORCE_PLAY
};
struct Override
{
enum Mode
{
ITEM_SET,
FRAME_ADD,
FRAME_SET
};
anm2::FrameOptional frame{};
int animationIndex{-1};
int sourceID{-1};
int destinationID{-1};
float length{-1.0f};
bool isLoop{false};
Mode mode{ITEM_SET};
anm2::Type type{anm2::LAYER};
bool isEnabled{true};
float time{};
};
anm2::Anm2* anm2{};
glm::vec2 position{};
float time{};
bool isPlaying{};
bool isShowNulls{};
int animationIndex{-1};
int previousAnimationIndex{-1};
int lastPlayedAnimationIndex{-1};
int playedEventID{-1};
Mode mode{PLAY};
float startTime{};
float speedMultiplier{};
std::unordered_set<int> playedTriggers{};
std::vector<Override*> overrides{};
Actor(anm2::Anm2*, glm::vec2, Mode = PLAY, float = 0.0f);
anm2::Animation* animation_get(int = -1);
anm2::Animation* animation_get(const std::string&);
int animation_index_get(const std::string&);
anm2::Item* item_get(anm2::Type, int = -1, int = -1);
int item_length(anm2::Item*);
anm2::Frame* trigger_get(int);
anm2::Frame* frame_get(int, anm2::Type, int = -1);
int item_id_get(const std::string&, anm2::Type = anm2::LAYER);
anm2::Frame frame_generate(anm2::Item&, float, anm2::Type, int = -1);
void play(const std::string&, Mode = PLAY, float = 0.0f, float = 1.0f);
void play(int, Mode = PLAY, float = 0.0f, float = 1.0f);
bool is_event(const std::string& event);
void tick();
bool is_playing(const std::string& name = {});
void render(Shader& textureShader, Shader& rectShader, Canvas&);
glm::vec4 null_frame_rect(int = -1);
void consume_played_event();
};
}

View File

@@ -1,234 +0,0 @@
#include "anm2.h"
#include <iostream>
#include "../util/xml_.h"
using namespace tinyxml2;
using namespace game::resource;
using namespace game::util;
namespace game::anm2
{
Info::Info(XMLElement* element)
{
if (!element) return;
element->QueryIntAttribute("Fps", &fps);
}
Spritesheet::Spritesheet(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_path_attribute(element, "Path", &path);
texture = Texture(path);
}
Layer::Layer(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("SpritesheetId", &spritesheetID);
}
Null::Null(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
element->QueryBoolAttribute("ShowRect", &isShowRect);
}
Event::Event(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_string_attribute(element, "Name", &name);
}
Sound::Sound(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
xml::query_path_attribute(element, "Path", &path);
audio = Audio(path);
}
Content::Content(XMLElement* element)
{
if (auto spritesheetsElement = element->FirstChildElement("Spritesheets"))
{
for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child;
child = child->NextSiblingElement("Spritesheet"))
{
int spritesheetId{};
Spritesheet spritesheet(child, spritesheetId);
spritesheets.emplace(spritesheetId, std::move(spritesheet));
}
}
if (auto layersElement = element->FirstChildElement("Layers"))
{
for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer"))
{
int layerId{};
Layer layer(child, layerId);
layers.emplace(layerId, std::move(layer));
}
}
if (auto nullsElement = element->FirstChildElement("Nulls"))
{
for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null"))
{
int nullId{};
Null null(child, nullId);
nulls.emplace(nullId, std::move(null));
}
}
if (auto eventsElement = element->FirstChildElement("Events"))
{
for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
{
int eventId{};
Event event(child, eventId);
events.emplace(eventId, std::move(event));
}
}
if (auto soundsElement = element->FirstChildElement("Sounds"))
{
for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
{
int soundId{};
Sound sound(child, soundId);
sounds.emplace(soundId, std::move(sound));
}
}
}
Frame::Frame(XMLElement* element, Type type)
{
if (type != TRIGGER)
{
element->QueryFloatAttribute("XPosition", &position.x);
element->QueryFloatAttribute("YPosition", &position.y);
if (type == LAYER)
{
element->QueryFloatAttribute("XPivot", &pivot.x);
element->QueryFloatAttribute("YPivot", &pivot.y);
element->QueryFloatAttribute("XCrop", &crop.x);
element->QueryFloatAttribute("YCrop", &crop.y);
element->QueryFloatAttribute("Width", &size.x);
element->QueryFloatAttribute("Height", &size.y);
}
element->QueryFloatAttribute("XScale", &scale.x);
element->QueryFloatAttribute("YScale", &scale.y);
element->QueryIntAttribute("Delay", &duration);
element->QueryBoolAttribute("Visible", &isVisible);
xml::query_color_attribute(element, "RedTint", &tint.r);
xml::query_color_attribute(element, "GreenTint", &tint.g);
xml::query_color_attribute(element, "BlueTint", &tint.b);
xml::query_color_attribute(element, "AlphaTint", &tint.a);
xml::query_color_attribute(element, "RedOffset", &colorOffset.r);
xml::query_color_attribute(element, "GreenOffset", &colorOffset.g);
xml::query_color_attribute(element, "BlueOffset", &colorOffset.b);
element->QueryFloatAttribute("Rotation", &rotation);
element->QueryBoolAttribute("Interpolated", &isInterpolated);
}
else
{
element->QueryIntAttribute("EventId", &eventID);
element->QueryIntAttribute("SoundId", &soundID);
element->QueryIntAttribute("AtFrame", &atFrame);
}
}
Item::Item(XMLElement* element, Type type, int& id)
{
if (type == LAYER) element->QueryIntAttribute("LayerId", &id);
if (type == NULL_) element->QueryIntAttribute("NullId", &id);
element->QueryBoolAttribute("Visible", &isVisible);
for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame");
child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame"))
frames.emplace_back(Frame(child, type));
}
Animation::Animation(XMLElement* element)
{
xml::query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("FrameNum", &frameNum);
element->QueryBoolAttribute("Loop", &isLoop);
int id{-1};
if (auto rootAnimationElement = element->FirstChildElement("RootAnimation"))
rootAnimation = Item(rootAnimationElement, ROOT, id);
if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations"))
{
for (auto child = layerAnimationsElement->FirstChildElement("LayerAnimation"); child;
child = child->NextSiblingElement("LayerAnimation"))
{
Item layerAnimation(child, LAYER, id);
layerOrder.push_back(id);
layerAnimations.emplace(id, std::move(layerAnimation));
}
}
if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
{
for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child;
child = child->NextSiblingElement("NullAnimation"))
{
Item nullAnimation(child, NULL_, id);
nullAnimations.emplace(id, std::move(nullAnimation));
}
}
if (auto triggersElement = element->FirstChildElement("Triggers")) triggers = Item(triggersElement, TRIGGER, id);
}
Animations::Animations(XMLElement* element)
{
xml::query_string_attribute(element, "DefaultAnimation", &defaultAnimation);
for (auto child = element->FirstChildElement("Animation"); child; child = child->NextSiblingElement("Animation"))
items.emplace_back(Animation(child));
for (int i = 0; i < items.size(); i++)
{
auto& item = items.at(i);
map[item.name] = i;
mapReverse[i] = item.name;
}
}
Anm2::Anm2(const std::filesystem::path& path)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
std::cout << "Failed to initialize anm2: " << document.ErrorStr() << "\n";
return;
}
auto previousPath = std::filesystem::current_path();
std::filesystem::current_path(path.parent_path());
auto element = document.RootElement();
if (auto infoElement = element->FirstChildElement("Info")) info = Info(infoElement);
if (auto contentElement = element->FirstChildElement("Content")) content = Content(contentElement);
if (auto animationsElement = element->FirstChildElement("Animations")) animations = Animations(animationsElement);
std::filesystem::current_path(previousPath);
std::cout << "Initialzed anm2: " << path.string() << "\n";
}
}

View File

@@ -1,176 +0,0 @@
#pragma once
#include <optional>
#include <tinyxml2/tinyxml2.h>
#include <filesystem>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
#include <glm/glm.hpp>
#include "audio.h"
#include "texture.h"
namespace game::anm2
{
enum Type
{
NONE,
ROOT,
LAYER,
NULL_,
TRIGGER
};
class Info
{
public:
int fps = 30;
Info() = default;
Info(tinyxml2::XMLElement*);
};
class Spritesheet
{
public:
std::filesystem::path path{};
resource::Texture texture{};
Spritesheet(tinyxml2::XMLElement*, int&);
};
class Layer
{
public:
std::string name{"New Layer"};
int spritesheetID{-1};
Layer(tinyxml2::XMLElement*, int&);
};
class Null
{
public:
std::string name{"New Null"};
bool isShowRect{};
Null(tinyxml2::XMLElement*, int&);
};
class Event
{
public:
std::string name{"New Event"};
Event(tinyxml2::XMLElement*, int&);
};
class Sound
{
public:
std::filesystem::path path{};
resource::Audio audio{};
Sound(tinyxml2::XMLElement*, int&);
};
class Content
{
public:
std::map<int, Spritesheet> spritesheets{};
std::map<int, Layer> layers{};
std::map<int, Null> nulls{};
std::map<int, Event> events{};
std::map<int, Sound> sounds{};
Content() = default;
Content(tinyxml2::XMLElement*);
};
struct Frame
{
glm::vec2 crop{};
glm::vec2 position{};
glm::vec2 pivot{};
glm::vec2 size{};
glm::vec2 scale{100, 100};
float rotation{};
int duration{};
glm::vec4 tint{1.0f, 1.0f, 1.0f, 1.0f};
glm::vec3 colorOffset{};
bool isInterpolated{};
int eventID{-1};
int soundID{-1};
int atFrame{-1};
bool isVisible{true};
Frame() = default;
Frame(tinyxml2::XMLElement*, Type);
};
struct FrameOptional
{
std::optional<glm::vec2> crop{};
std::optional<glm::vec2> position{};
std::optional<glm::vec2> pivot{};
std::optional<glm::vec2> size{};
std::optional<glm::vec2> scale{};
std::optional<float> rotation{};
std::optional<glm::vec4> tint{};
std::optional<glm::vec3> colorOffset{};
std::optional<bool> isInterpolated{};
std::optional<bool> isVisible{};
};
class Item
{
public:
std::vector<Frame> frames{};
bool isVisible{};
Item() = default;
Item(tinyxml2::XMLElement*, Type, int&);
};
class Animation
{
public:
std::string name{"New Animation"};
int frameNum{};
bool isLoop{};
Item rootAnimation{};
std::unordered_map<int, Item> layerAnimations{};
std::vector<int> layerOrder{};
std::map<int, Item> nullAnimations{};
Item triggers{};
Animation() = default;
Animation(tinyxml2::XMLElement*);
};
class Animations
{
public:
std::string defaultAnimation{};
std::vector<Animation> items{};
std::unordered_map<std::string, int> map{};
std::unordered_map<int, std::string> mapReverse{};
Animations() = default;
Animations(tinyxml2::XMLElement*);
};
class Anm2
{
public:
Info info;
Content content{};
Animations animations{};
Anm2() = default;
Anm2(const std::filesystem::path&);
};
}

View File

@@ -1,67 +1,122 @@
#include "audio.h"
#include "audio.hpp"
#include <SDL3/SDL_properties.h>
#include <iostream>
#include "../log.hpp"
#include <string>
#include <format>
#include <unordered_map>
using namespace game::util;
namespace game::resource
{
static std::shared_ptr<MIX_Audio> audio_make(MIX_Audio* audio)
{
return std::shared_ptr<MIX_Audio>(audio,
[](MIX_Audio* a)
{
if (a) MIX_DestroyAudio(a);
});
}
static std::unordered_map<std::string, std::weak_ptr<MIX_Audio>> audioCache{};
static std::shared_ptr<MIX_Audio> cache_get(const std::string& key)
{
auto it = audioCache.find(key);
if (it == audioCache.end()) return {};
auto cached = it->second.lock();
if (!cached) audioCache.erase(it);
return cached;
}
static void cache_set(const std::string& key, const std::shared_ptr<MIX_Audio>& audio)
{
if (!audio) return;
audioCache[key] = audio;
}
MIX_Mixer* Audio::mixer_get()
{
static auto mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
return mixer;
}
void Audio::retain()
void Audio::volume_set(float volume)
{
if (refCount) ++(*refCount);
}
void Audio::release()
{
if (refCount)
{
if (--(*refCount) == 0)
{
if (internal) MIX_DestroyAudio(internal);
delete refCount;
}
refCount = nullptr;
}
internal = nullptr;
auto mixer = mixer_get();
MIX_SetMasterGain(mixer, volume);
}
Audio::Audio(const std::filesystem::path& path)
{
internal = MIX_LoadAudio(mixer_get(), path.c_str(), true);
auto pathString = path.string();
auto key = std::string("fs:") + pathString;
internal = cache_get(key);
if (internal)
{
refCount = new int(1);
std::cout << "Initialized audio: '" << path.string() << "'\n";
logger.info(std::format("Using cached audio: {}", pathString));
return;
}
else
internal = audio_make(MIX_LoadAudio(mixer_get(), pathString.c_str(), true));
cache_set(key, internal);
if (internal) logger.info(std::format("Initialized audio: {}", pathString));
if (!internal) logger.info(std::format("Failed to intialize audio: {} ({})", pathString, SDL_GetError()));
}
Audio::Audio(const physfs::Path& path)
{
if (!path.is_valid())
{
std::cout << "Failed to initialize audio: '" << path.string() << "'\n";
logger.error(
std::format("Failed to initialize audio from PhysicsFS path: {}", path.c_str(), physfs::error_get()));
return;
}
auto key = std::string("physfs:") + path.c_str();
internal = cache_get(key);
if (internal)
{
logger.info(std::format("Using cached audio: {}", path.c_str()));
return;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(
std::format("Failed to initialize audio from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
return;
}
auto ioStream = SDL_IOFromConstMem(buffer.data(), buffer.size());
internal = audio_make(MIX_LoadAudio_IO(mixer_get(), ioStream, false, true));
cache_set(key, internal);
if (internal)
logger.info(std::format("Initialized audio: {}", path.c_str()));
else
logger.info(std::format("Failed to intialize audio: {} ({})", path.c_str(), SDL_GetError()));
}
Audio::Audio(const Audio& other)
{
internal = other.internal;
refCount = other.refCount;
retain();
track = nullptr;
}
Audio::Audio(Audio&& other) noexcept
{
internal = other.internal;
internal = std::move(other.internal);
track = other.track;
refCount = other.refCount;
other.internal = nullptr;
other.track = nullptr;
other.refCount = nullptr;
}
Audio& Audio::operator=(const Audio& other)
@@ -70,8 +125,7 @@ namespace game::resource
{
unload();
internal = other.internal;
refCount = other.refCount;
retain();
track = nullptr;
}
return *this;
}
@@ -81,13 +135,10 @@ namespace game::resource
if (this != &other)
{
unload();
internal = other.internal;
internal = std::move(other.internal);
track = other.track;
refCount = other.refCount;
other.internal = nullptr;
other.track = nullptr;
other.refCount = nullptr;
}
return *this;
}
@@ -99,7 +150,7 @@ namespace game::resource
MIX_DestroyTrack(track);
track = nullptr;
}
release();
internal.reset();
}
void Audio::play(bool isLoop)
@@ -120,7 +171,7 @@ namespace game::resource
if (!track) return;
}
MIX_SetTrackAudio(track, internal);
MIX_SetTrackAudio(track, internal.get());
SDL_PropertiesID options = 0;
@@ -143,5 +194,5 @@ namespace game::resource
bool Audio::is_playing() const { return track && MIX_TrackPlaying(track); }
Audio::~Audio() { unload(); }
bool Audio::is_valid() const { return internal != nullptr; }
bool Audio::is_valid() const { return (bool)internal; }
}

View File

@@ -2,22 +2,24 @@
#include <SDL3_mixer/SDL_mixer.h>
#include <filesystem>
#include <memory>
#include "../util/physfs.hpp"
namespace game::resource
{
class Audio
{
MIX_Audio* internal{nullptr};
MIX_Track* track{nullptr};
int* refCount{nullptr};
MIX_Mixer* mixer_get();
static MIX_Mixer* mixer_get();
void unload();
void retain();
void release();
std::shared_ptr<MIX_Audio> internal{};
MIX_Track* track{nullptr};
public:
Audio() = default;
Audio(const std::filesystem::path&);
Audio(const util::physfs::Path&);
Audio(const Audio&);
Audio(Audio&&) noexcept;
Audio& operator=(const Audio&);
@@ -27,5 +29,6 @@ namespace game::resource
void play(bool isLoop = false);
void stop();
bool is_playing() const;
static void volume_set(float volume);
};
}

View File

@@ -1,137 +0,0 @@
#include "dialogue.h"
#include <iostream>
#include "../util/map_.h"
#include "../util/xml_.h"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource
{
void label_map_query(XMLElement* element, std::map<std::string, int>& labelMap, const char* attribute, int& id)
{
std::string label{};
xml::query_string_attribute(element, attribute, &label);
if (auto foundID = map::find(labelMap, label))
id = *foundID;
else
id = -1;
}
Dialogue::Color::Color(XMLElement* element)
{
if (!element) return;
element->QueryIntAttribute("Start", &start);
element->QueryIntAttribute("End", &end);
xml::query_color_attribute(element, "R", &value.r);
xml::query_color_attribute(element, "G", &value.g);
xml::query_color_attribute(element, "B", &value.b);
xml::query_color_attribute(element, "A", &value.a);
}
Dialogue::Animation::Animation(XMLElement* element)
{
if (!element) return;
element->QueryIntAttribute("At", &at);
xml::query_string_attribute(element, "Name", &name);
}
Dialogue::Branch::Branch(XMLElement* element, std::map<std::string, int>& labelMap)
{
if (!element) return;
label_map_query(element, labelMap, "Label", nextID);
xml::query_string_attribute(element, "Content", &content);
}
Dialogue::Entry::Entry(XMLElement* element, std::map<std::string, int>& labelMap)
{
if (!element) return;
xml::query_string_attribute(element, "Content", &content);
label_map_query(element, labelMap, "Next", nextID);
std::string flagString{};
xml::query_string_attribute(element, "Flag", &flagString);
for (int i = 0; i < std::size(FLAG_STRINGS); i++)
{
if (flagString == FLAG_STRINGS[i])
{
flag = (Flag)i;
break;
}
}
for (auto child = element->FirstChildElement("Color"); child; child = child->NextSiblingElement("Color"))
colors.emplace_back(child);
for (auto child = element->FirstChildElement("Animation"); child; child = child->NextSiblingElement("Animation"))
animations.emplace_back(child);
for (auto child = element->FirstChildElement("Branch"); child; child = child->NextSiblingElement("Branch"))
branches.emplace_back(child, labelMap);
}
Dialogue::Dialogue(const std::string& path)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
std::cout << "Failed to initialize dialogue: " << document.ErrorStr() << "\n";
return;
}
auto element = document.RootElement();
int id{};
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
{
std::string label{};
xml::query_string_attribute(child, "Label", &label);
labelMap.emplace(label, id++);
}
id = 0;
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
entryMap.emplace(id++, Entry(child, labelMap));
for (auto& [label, id] : labelMap)
{
if (label.starts_with(BURP_SMALL_LABEL)) burpSmallIDs.emplace_back(id);
if (label.starts_with(BURP_BIG_LABEL)) burpBigIDs.emplace_back(id);
if (label.starts_with(EAT_HUNGRY_LABEL)) eatHungryIDs.emplace_back(id);
if (label.starts_with(EAT_FULL_LABEL)) eatFullIDs.emplace_back(id);
if (label.starts_with(FULL_LABEL)) fullIDs.emplace_back(id);
if (label.starts_with(CAPACITY_LOW_LABEL)) capacityLowIDs.emplace_back(id);
if (label.starts_with(FEED_HUNGRY_LABEL)) feedHungryIDs.emplace_back(id);
if (label.starts_with(FEED_FULL_LABEL)) feedFullIDs.emplace_back(id);
if (label.starts_with(FOOD_STOLEN_LABEL)) foodStolenIDs.emplace_back(id);
if (label.starts_with(FOOD_EASED_LABEL)) foodEasedIDs.emplace_back(id);
if (label.starts_with(PERFECT_LABEL)) perfectIDs.emplace_back(id);
if (label.starts_with(MISS_LABEL)) missIDs.emplace_back(id);
if (label.starts_with(POST_DIGEST_LABEL)) postDigestIDs.emplace_back(id);
if (label.starts_with(RANDOM_LABEL)) randomIDs.emplace_back(id);
}
std::cout << "Initialzed dialogue: " << path << "\n";
}
Dialogue::Entry* Dialogue::get(int id)
{
if (id == -1) return nullptr;
return map::find(entryMap, id);
}
Dialogue::Entry* Dialogue::get(const std::string& label)
{
auto id = map::find(labelMap, label);
if (!id) return nullptr;
return get(*id);
}
Dialogue::Entry* Dialogue::next(Dialogue::Entry* entry) { return get(entry->nextID); }
}

View File

@@ -1,118 +0,0 @@
#pragma once
#include "glm/ext/vector_float4.hpp"
#include <tinyxml2.h>
#include <string>
#include <map>
namespace game::resource
{
class Dialogue
{
public:
static constexpr auto FULL_LABEL = "Full";
static constexpr auto POST_DIGEST_LABEL = "PostDigest";
static constexpr auto BURP_SMALL_LABEL = "BurpSmall";
static constexpr auto BURP_BIG_LABEL = "BurpBig";
static constexpr auto FEED_HUNGRY_LABEL = "FeedHungry";
static constexpr auto FEED_FULL_LABEL = "FeedFull";
static constexpr auto EAT_HUNGRY_LABEL = "EatHungry";
static constexpr auto EAT_FULL_LABEL = "EatFull";
static constexpr auto FOOD_STOLEN_LABEL = "FoodStolen";
static constexpr auto FOOD_EASED_LABEL = "FoodEased";
static constexpr auto CAPACITY_LOW_LABEL = "CapacityLow";
static constexpr auto PERFECT_LABEL = "Perfect";
static constexpr auto MISS_LABEL = "Miss";
static constexpr auto RANDOM_LABEL = "StartRandom";
;
class Color
{
public:
int start{};
int end{};
glm::vec4 value{};
Color(tinyxml2::XMLElement*);
};
class Animation
{
public:
int at{-1};
std::string name{};
Animation(tinyxml2::XMLElement*);
};
class Branch
{
public:
std::string content{};
int nextID{-1};
Branch(tinyxml2::XMLElement*, std::map<std::string, int>&);
};
class Entry
{
public:
#define FLAGS \
X(NONE, "None") \
X(ACTIVATE_WINDOWS, "ActivateWindows") \
X(DEACTIVATE_WINDOWS, "DeactivateWindows") \
X(ONLY_INFO, "OnlyInfo") \
X(ACTIVATE_CHEATS, "ActivateCheats")
enum Flag
{
#define X(symbol, name) symbol,
FLAGS
#undef X
};
static constexpr const char* FLAG_STRINGS[] = {
#define X(symbol, name) name,
FLAGS
#undef X
};
#undef FLAGS
std::string content{};
std::vector<Color> colors{};
std::vector<Animation> animations{};
std::vector<Branch> branches{};
int nextID{-1};
Flag flag{Flag::NONE};
Entry(tinyxml2::XMLElement*, std::map<std::string, int>&);
};
std::map<std::string, int> labelMap;
std::map<int, Entry> entryMap{};
std::vector<int> eatHungryIDs{};
std::vector<int> eatFullIDs{};
std::vector<int> feedHungryIDs{};
std::vector<int> feedFullIDs{};
std::vector<int> burpSmallIDs{};
std::vector<int> burpBigIDs{};
std::vector<int> fullIDs{};
std::vector<int> foodStolenIDs{};
std::vector<int> foodEasedIDs{};
std::vector<int> perfectIDs{};
std::vector<int> postDigestIDs{};
std::vector<int> missIDs{};
std::vector<int> capacityLowIDs{};
std::vector<int> randomIDs{};
Dialogue(const std::string&);
Entry* get(const std::string&);
Entry* get(int = -1);
Entry* next(Entry*);
};
}

View File

@@ -1,10 +1,112 @@
#include "font.h"
#include "font.hpp"
#include "../log.hpp"
#include <format>
using namespace game::util;
static ImFont* default_font_fallback_get()
{
auto& io = ImGui::GetIO();
if (io.FontDefault) return io.FontDefault;
if (!io.Fonts->Fonts.empty()) return io.Fonts->Fonts.front();
return io.Fonts->AddFontDefault();
}
namespace game::resource
{
Font::Font(const std::string& path, float size)
Font::Font(const std::filesystem::path& path, float size)
{
internal = ImGui::GetIO().Fonts->AddFontFromFileTTF(path.c_str(), size);
auto pathString = path.string();
std::error_code ec;
if (!std::filesystem::is_regular_file(path, ec))
{
logger.error(std::format("Failed to initialize font: {} (file not found)", pathString));
internal = default_font_fallback_get();
if (internal)
logger.info(std::format("Falling back to default ImGui font: {}", pathString));
else
logger.error("Failed to initialize fallback ImGui default font");
return;
}
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
internal = ImGui::GetIO().Fonts->AddFontFromFileTTF(pathString.c_str(), size, &config);
if (internal)
logger.info(std::format("Initialized font: {}", pathString));
else
{
logger.error(std::format("Failed to initialize font: {}", pathString));
internal = default_font_fallback_get();
if (internal)
logger.info(std::format("Falling back to default ImGui font: {}", pathString));
else
logger.error("Failed to initialize fallback ImGui default font");
}
}
Font::Font(const physfs::Path& path, float size)
{
if (!path.is_valid())
{
logger.error(
std::format("Failed to initialize font from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
internal = default_font_fallback_get();
if (internal)
logger.info(std::format("Falling back to default ImGui font: {}", path.c_str()));
else
logger.error("Failed to initialize fallback ImGui default font");
return;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(
std::format("Failed to initialize font from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
internal = default_font_fallback_get();
if (internal)
logger.info(std::format("Falling back to default ImGui font: {}", path.c_str()));
else
logger.error("Failed to initialize fallback ImGui default font");
return;
}
if (buffer.size() <= 100)
{
logger.error(std::format("Failed to initialize font from PhysicsFS path: {} (buffer too small)", path.c_str()));
internal = default_font_fallback_get();
if (internal)
logger.info(std::format("Falling back to default ImGui font: {}", path.c_str()));
else
logger.error("Failed to initialize fallback ImGui default font");
return;
}
ImFontConfig config;
config.FontDataOwnedByAtlas = false;
internal = ImGui::GetIO().Fonts->AddFontFromMemoryTTF(buffer.data(), (int)buffer.size(), size, &config);
if (internal)
logger.info(std::format("Initialized font: {}", path.c_str()));
else
{
logger.error(std::format("Failed to initialize font: {}", path.c_str()));
internal = default_font_fallback_get();
if (internal)
logger.info(std::format("Falling back to default ImGui font: {}", path.c_str()));
else
logger.error("Failed to initialize fallback ImGui default font");
}
}
ImFont* Font::get() { return internal; };
}

View File

@@ -1,20 +0,0 @@
#pragma once
#include "imgui.h"
#include <string>
namespace game::resource
{
class Font
{
public:
ImFont* internal;
static constexpr auto NORMAL = 12;
static constexpr auto BIG = 16;
static constexpr auto LARGE = 24;
Font(const std::string&, float = NORMAL);
ImFont* get();
};
}

28
src/resource/font.hpp Normal file
View File

@@ -0,0 +1,28 @@
#pragma once
#include <filesystem>
#include <imgui.h>
#include "../util/physfs.hpp"
namespace game::resource
{
class Font
{
ImFont* internal{};
public:
static constexpr auto NORMAL = 20;
static constexpr auto ABOVE_AVERAGE = 24;
static constexpr auto BIG = 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);
Font(const util::physfs::Path&, float = NORMAL);
ImFont* get();
};
}

View File

@@ -1,6 +1,8 @@
#include "shader.h"
#include "shader.hpp"
#include <iostream>
#include "../log.hpp"
#include <format>
namespace game::resource
{
@@ -20,7 +22,7 @@ namespace game::resource
glGetShaderiv(shaderHandle, GL_INFO_LOG_LENGTH, &logLength);
std::string log(logLength, '\0');
if (logLength > 0) glGetShaderInfoLog(shaderHandle, logLength, nullptr, log.data());
std::cout << "Failed to compile shader: " << log << '\n';
logger.error(std::format("Failed to compile {} shader: {}", stage, log));
return false;
}
return true;
@@ -49,11 +51,11 @@ namespace game::resource
if (!isLinked)
{
glDeleteProgram(id);
logger.error(std::format("Failed to link shader: {}", id));
id = 0;
std::cout << "Failed to link shader: " << id << "\n";
}
else
std::cout << "Initialized shader: " << id << "\n";
logger.info(std::format("Initialized shader: {}", id));
glDeleteShader(vertexHandle);
glDeleteShader(fragmentHandle);

View File

@@ -1,60 +1,76 @@
#include "texture.h"
#include "texture.hpp"
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#pragma GCC diagnostic ignored "-Wunused-function"
#endif
#include <SDL3/SDL_surface.h>
#include <string>
#include <format>
#include <unordered_map>
#define STBI_ONLY_PNG
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
#include <iostream>
#include "../log.hpp"
using namespace glm;
using namespace game::util;
namespace game::resource
{
struct CachedTexture
{
std::weak_ptr<GLuint> idShared{};
glm::ivec2 size{};
int channels{};
};
static std::unordered_map<std::string, CachedTexture> textureCache{};
static bool cache_get(const std::string& key, std::shared_ptr<GLuint>& idShared, GLuint& id, ivec2& size, int& channels)
{
auto it = textureCache.find(key);
if (it == textureCache.end()) return false;
auto shared = it->second.idShared.lock();
if (!shared)
{
textureCache.erase(it);
return false;
}
idShared = shared;
id = *shared;
size = it->second.size;
channels = it->second.channels;
return true;
}
static void cache_set(const std::string& key, const std::shared_ptr<GLuint>& idShared, ivec2 size, int channels)
{
if (!idShared) return;
textureCache[key] = CachedTexture{.idShared = idShared, .size = size, .channels = channels};
}
static std::shared_ptr<GLuint> texture_id_make(GLuint id)
{
return std::shared_ptr<GLuint>(new GLuint(id),
[](GLuint* p)
{
if (!p) return;
if (*p != 0) glDeleteTextures(1, p);
delete p;
});
}
bool Texture::is_valid() const { return id != 0; }
void Texture::retain()
Texture::~Texture()
{
if (refCount) ++(*refCount);
}
void Texture::release()
{
if (refCount)
{
if (--(*refCount) == 0)
{
if (is_valid()) glDeleteTextures(1, &id);
delete refCount;
}
refCount = nullptr;
}
else if (is_valid())
{
glDeleteTextures(1, &id);
}
idShared.reset();
id = 0;
}
Texture::~Texture() { release(); }
Texture::Texture(const Texture& other)
{
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
retain();
idShared = other.idShared;
}
Texture::Texture(Texture&& other) noexcept
@@ -62,24 +78,21 @@ namespace game::resource
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
idShared = std::move(other.idShared);
other.id = 0;
other.size = {};
other.channels = 0;
other.refCount = nullptr;
}
Texture& Texture::operator=(const Texture& other)
{
if (this != &other)
{
release();
idShared = other.idShared;
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
retain();
}
return *this;
}
@@ -88,45 +101,104 @@ namespace game::resource
{
if (this != &other)
{
release();
idShared.reset();
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
idShared = std::move(other.idShared);
other.id = 0;
other.size = {};
other.channels = 0;
other.refCount = nullptr;
}
return *this;
}
void Texture::init(const uint8_t* data)
{
idShared.reset();
id = 0;
GLuint newId{};
glGenTextures(1, &newId);
glBindTexture(GL_TEXTURE_2D, newId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, 0);
channels = CHANNELS;
idShared = texture_id_make(newId);
id = newId;
}
Texture::Texture(const std::filesystem::path& path)
{
if (auto data = stbi_load(path.c_str(), &size.x, &size.y, nullptr, CHANNELS); data)
auto pathString = path.string();
auto key = std::string("fs:") + pathString;
if (cache_get(key, idShared, id, size, channels))
{
glGenTextures(1, &id);
glBindTexture(GL_TEXTURE_2D, id);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, 0);
stbi_image_free(data);
channels = CHANNELS;
refCount = new int(1);
std::cout << "Initialized texture: '" << path.string() << "\n";
logger.info(std::format("Using cached texture: {}", pathString));
return;
}
if (auto surface = SDL_LoadPNG(pathString.c_str()))
{
auto rgbaSurface = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
SDL_DestroySurface(surface);
surface = rgbaSurface;
this->size = ivec2(surface->w, surface->h);
init((const uint8_t*)surface->pixels);
SDL_DestroySurface(surface);
cache_set(key, idShared, this->size, channels);
logger.info(std::format("Initialized texture: {}", pathString));
}
else
logger.error(std::format("Failed to initialize texture: {} ({})", pathString, SDL_GetError()));
}
Texture::Texture(const physfs::Path& path)
{
if (!path.is_valid())
{
id = 0;
size = {};
channels = 0;
refCount = nullptr;
std::cout << "Failed to initialize texture: '" << path.string() << "'\n";
logger.error(
std::format("Failed to initialize texture from PhysicsFS path: {}", path.c_str(), physfs::error_get()));
return;
}
auto key = std::string("physfs:") + path.c_str();
if (cache_get(key, idShared, id, size, channels))
{
logger.info(std::format("Using cached texture: {}", path.c_str()));
return;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(
std::format("Failed to initialize texture from PhysicsFS path: {} ({})", path.c_str(), physfs::error_get()));
return;
}
auto ioStream = SDL_IOFromConstMem(buffer.data(), buffer.size());
if (auto surface = SDL_LoadPNG_IO(ioStream, true))
{
auto rgbaSurface = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
SDL_DestroySurface(surface);
surface = rgbaSurface;
this->size = ivec2(surface->w, surface->h);
init((const uint8_t*)surface->pixels);
SDL_DestroySurface(surface);
cache_set(key, idShared, this->size, channels);
logger.info(std::format("Initialized texture: {}", path.c_str()));
}
else
logger.error(std::format("Failed to initialize texture: {} ({})", path.c_str(), SDL_GetError()));
}
}

View File

@@ -6,8 +6,12 @@
#include <glad/glad.h>
#endif
#include <cstdint>
#include <filesystem>
#include <glm/ext/vector_int2.hpp>
#include <memory>
#include "../util/physfs.hpp"
namespace game::resource
{
@@ -29,10 +33,10 @@ namespace game::resource
Texture& operator=(const Texture&);
Texture& operator=(Texture&&) noexcept;
Texture(const std::filesystem::path&);
Texture(const util::physfs::Path&);
void init(const uint8_t*);
private:
int* refCount{nullptr};
void retain();
void release();
std::shared_ptr<GLuint> idShared{};
};
}

View File

@@ -0,0 +1,12 @@
#include "animation_entry.hpp"
#include "../../util/vector.hpp"
namespace game::resource::xml
{
std::string* AnimationEntryCollection::get()
{
if (empty()) return nullptr;
return &at(util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; })).animation;
}
}

View File

@@ -0,0 +1,23 @@
// Handles animation entries in .xml files. "Weight" value determines weight of being randomly selected.
#pragma once
#include <string>
#include <vector>
namespace game::resource::xml
{
class AnimationEntry
{
public:
std::string animation{};
float weight{1.0f};
};
class AnimationEntryCollection : public std::vector<AnimationEntry>
{
public:
std::string* get();
};
}

340
src/resource/xml/anm2.cpp Normal file
View File

@@ -0,0 +1,340 @@
#include "anm2.hpp"
#include "util.hpp"
#include "../../util/working_directory.hpp"
#include "../../log.hpp"
#include <format>
#include <ranges>
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Anm2::Anm2(const Anm2&) = default;
Anm2::Anm2(Anm2&&) = default;
Anm2& Anm2::operator=(const Anm2&) = default;
Anm2& Anm2::operator=(Anm2&&) = default;
void Anm2::init(XMLDocument& document, Flags anm2Flags, const physfs::Path& archive)
{
this->flags = anm2Flags;
auto element = document.RootElement();
if (!element) return;
auto parse_frame = [&](XMLElement* element, Type type, int spritesheetID = -1)
{
Frame frame{};
if (!element) return frame;
if (type != TRIGGER)
{
element->QueryFloatAttribute("XPosition", &frame.position.x);
element->QueryFloatAttribute("YPosition", &frame.position.y);
if (type == LAYER)
{
if (element->FindAttribute("RegionId") && spritesheets.contains(spritesheetID))
{
auto& spritesheet = spritesheets.at(spritesheetID);
element->QueryIntAttribute("RegionId", &frame.regionID);
auto& region = spritesheet.regions.at(frame.regionID);
frame.crop = region.crop;
frame.size = region.size;
frame.pivot = region.origin == Spritesheet::Region::Origin::CENTER
? glm::vec2(glm::ivec2(frame.size * 0.5f))
: region.origin == Spritesheet::Region::Origin::TOP_LEFT ? glm::vec2{}
: region.pivot;
}
else
{
element->QueryFloatAttribute("XPivot", &frame.pivot.x);
element->QueryFloatAttribute("YPivot", &frame.pivot.y);
element->QueryFloatAttribute("XCrop", &frame.crop.x);
element->QueryFloatAttribute("YCrop", &frame.crop.y);
element->QueryFloatAttribute("Width", &frame.size.x);
element->QueryFloatAttribute("Height", &frame.size.y);
}
}
element->QueryFloatAttribute("XScale", &frame.scale.x);
element->QueryFloatAttribute("YScale", &frame.scale.y);
element->QueryIntAttribute("Delay", &frame.duration);
element->QueryBoolAttribute("Visible", &frame.isVisible);
xml::query_color_attribute(element, "RedTint", &frame.tint.r);
xml::query_color_attribute(element, "GreenTint", &frame.tint.g);
xml::query_color_attribute(element, "BlueTint", &frame.tint.b);
xml::query_color_attribute(element, "AlphaTint", &frame.tint.a);
xml::query_color_attribute(element, "RedOffset", &frame.colorOffset.r);
xml::query_color_attribute(element, "GreenOffset", &frame.colorOffset.g);
xml::query_color_attribute(element, "BlueOffset", &frame.colorOffset.b);
element->QueryFloatAttribute("Rotation", &frame.rotation);
element->QueryBoolAttribute("Interpolated", &frame.isInterpolated);
}
else
{
for (auto child = element->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
{
int soundID{};
child->QueryIntAttribute("Id", &soundID);
frame.soundIDs.emplace_back(soundID);
}
element->QueryIntAttribute("EventId", &frame.eventID);
element->QueryIntAttribute("AtFrame", &frame.atFrame);
}
return frame;
};
auto parse_item = [&](XMLElement* element, Type type, int& id)
{
Item item{};
if (!element) return item;
if (type == LAYER) element->QueryIntAttribute("LayerId", &id);
if (type == NULL_) element->QueryIntAttribute("NullId", &id);
element->QueryBoolAttribute("Visible", &item.isVisible);
for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame");
child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame"))
item.frames.emplace_back(parse_frame(child, type, type == LAYER ? layers.at(id).spritesheetID : -1));
return item;
};
auto parse_animation = [&](XMLElement* element)
{
Animation animation{};
if (!element) return animation;
xml::query_string_attribute(element, "Name", &animation.name);
element->QueryIntAttribute("FrameNum", &animation.frameNum);
element->QueryBoolAttribute("Loop", &animation.isLoop);
int id{-1};
if (auto rootAnimationElement = element->FirstChildElement("RootAnimation"))
animation.rootAnimation = parse_item(rootAnimationElement, ROOT, id);
if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations"))
{
for (auto child = layerAnimationsElement->FirstChildElement("LayerAnimation"); child;
child = child->NextSiblingElement("LayerAnimation"))
{
auto layerAnimation = parse_item(child, LAYER, id);
animation.layerOrder.push_back(id);
animation.layerAnimations.emplace(id, std::move(layerAnimation));
}
}
if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
{
for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child;
child = child->NextSiblingElement("NullAnimation"))
{
auto nullAnimation = parse_item(child, NULL_, id);
animation.nullAnimations.emplace(id, std::move(nullAnimation));
}
}
if (auto triggersElement = element->FirstChildElement("Triggers"))
animation.triggers = parse_item(triggersElement, TRIGGER, id);
return animation;
};
if (auto infoElement = element->FirstChildElement("Info")) infoElement->QueryIntAttribute("Fps", &fps);
if (auto contentElement = element->FirstChildElement("Content"))
{
if (auto spritesheetsElement = contentElement->FirstChildElement("Spritesheets"))
{
for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child;
child = child->NextSiblingElement("Spritesheet"))
{
int spritesheetId{};
Spritesheet spritesheet{};
child->QueryIntAttribute("Id", &spritesheetId);
xml::query_string_attribute(child, "Path", &spritesheet.path);
if ((this->flags & NO_SPRITESHEETS) != 0)
spritesheet.texture = Texture();
else if (!archive.empty())
spritesheet.texture = Texture(physfs::Path(archive + "/" + spritesheet.path));
else
spritesheet.texture = Texture(std::filesystem::path(spritesheet.path));
for (auto regionChild = child->FirstChildElement("Region"); regionChild;
regionChild = regionChild->NextSiblingElement("Region"))
{
Spritesheet::Region region{};
int regionID{};
regionChild->QueryIntAttribute("Id", &regionID);
xml::query_string_attribute(regionChild, "Name", &region.name);
regionChild->QueryFloatAttribute("XCrop", &region.crop.x);
regionChild->QueryFloatAttribute("YCrop", &region.crop.y);
regionChild->QueryFloatAttribute("Width", &region.size.x);
regionChild->QueryFloatAttribute("Height", &region.size.y);
if (regionChild->FindAttribute("Origin"))
{
std::string origin{};
xml::query_string_attribute(regionChild, "Origin", &origin);
region.origin = origin == "Center" ? Spritesheet::Region::CENTER
: origin == "TopLeft" ? Spritesheet::Region::TOP_LEFT
: Spritesheet::Region::CUSTOM;
}
else
{
regionChild->QueryFloatAttribute("XPivot", &region.pivot.x);
regionChild->QueryFloatAttribute("YPivot", &region.pivot.y);
}
spritesheet.regions.emplace(regionID, std::move(region));
spritesheet.regionOrder.emplace_back(regionID);
}
spritesheets.emplace(spritesheetId, std::move(spritesheet));
}
}
if (auto layersElement = contentElement->FirstChildElement("Layers"))
{
for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer"))
{
int layerId{};
Layer layer{};
child->QueryIntAttribute("Id", &layerId);
xml::query_string_attribute(child, "Name", &layer.name);
child->QueryIntAttribute("SpritesheetId", &layer.spritesheetID);
layerMap[layer.name] = layerId;
layers.emplace(layerId, std::move(layer));
}
}
if (auto nullsElement = contentElement->FirstChildElement("Nulls"))
{
for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null"))
{
int nullId{};
Null null{};
child->QueryIntAttribute("Id", &nullId);
xml::query_string_attribute(child, "Name", &null.name);
child->QueryBoolAttribute("ShowRect", &null.isShowRect);
nullMap[null.name] = nullId;
nulls.emplace(nullId, std::move(null));
}
}
if (auto eventsElement = contentElement->FirstChildElement("Events"))
{
for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
{
int eventId{};
Event event{};
child->QueryIntAttribute("Id", &eventId);
xml::query_string_attribute(child, "Name", &event.name);
eventMap[event.name] = eventId;
events.emplace(eventId, std::move(event));
}
}
if (auto soundsElement = contentElement->FirstChildElement("Sounds"))
{
for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
{
int soundId{};
Sound sound{};
child->QueryIntAttribute("Id", &soundId);
xml::query_string_attribute(child, "Path", &sound.path);
if ((this->flags & NO_SOUNDS) != 0)
sound.audio = Audio();
else if (!archive.empty())
sound.audio = Audio(physfs::Path(archive + "/" + sound.path));
else
sound.audio = Audio(std::filesystem::path(sound.path));
sounds.emplace(soundId, std::move(sound));
}
}
}
if (auto animationsElement = element->FirstChildElement("Animations"))
{
xml::query_string_attribute(animationsElement, "DefaultAnimation", &defaultAnimation);
for (auto child = animationsElement->FirstChildElement("Animation"); child;
child = child->NextSiblingElement("Animation"))
{
if ((this->flags & DEFAULT_ANIMATION_ONLY) != 0)
{
std::string name{};
xml::query_string_attribute(child, "Name", &name);
if (name == defaultAnimation)
{
animations.emplace_back(parse_animation(child));
break;
}
}
else
animations.emplace_back(parse_animation(child));
}
for (int i = 0; i < (int)animations.size(); i++)
{
auto& animation = animations[i];
animationMap[animation.name] = i;
animationMapReverse[i] = animation.name;
}
if (animationMap.contains(defaultAnimation))
defaultAnimationID = animationMap[defaultAnimation];
else
defaultAnimationID = -1;
}
isValid = true;
}
Anm2::Anm2(const std::filesystem::path& path, Flags anm2Flags)
{
XMLDocument document;
auto pathString = path.string();
if (document.LoadFile(pathString.c_str()) != XML_SUCCESS)
{
logger.error(std::format("Failed to initialize anm2: {} ({})", pathString, document.ErrorStr()));
isValid = false;
return;
}
WorkingDirectory workingDirectory(path, WorkingDirectory::FILE);
this->path = path.string();
init(document, anm2Flags);
logger.info(std::format("Initialized anm2: {}", pathString));
}
Anm2::Anm2(const physfs::Path& path, Flags anm2Flags)
{
XMLDocument document;
if (xml::document_load(path, document) != XML_SUCCESS)
{
isValid = false;
return;
}
this->path = path;
init(document, anm2Flags, path.directory_get());
logger.info(std::format("Initialized anm2: {}", path.c_str()));
}
bool Anm2::is_valid() const { return isValid; }
}

177
src/resource/xml/anm2.hpp Normal file
View File

@@ -0,0 +1,177 @@
#pragma once
#include <optional>
#include <tinyxml2/tinyxml2.h>
#include <filesystem>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
#include <glm/glm.hpp>
#include "../audio.hpp"
#include "../texture.hpp"
#include "../../util/physfs.hpp"
namespace game::resource::xml
{
class Anm2
{
public:
enum Type
{
NONE,
ROOT,
LAYER,
NULL_,
TRIGGER
};
enum Flag
{
NO_SPRITESHEETS = (1 << 0),
NO_SOUNDS = (1 << 1),
DEFAULT_ANIMATION_ONLY = (1 << 2)
};
using Flags = int;
struct Spritesheet
{
struct Region
{
enum Origin
{
TOP_LEFT,
CENTER,
CUSTOM
};
std::string name{};
glm::vec2 crop{};
glm::vec2 pivot{};
glm::vec2 size{};
Origin origin{CUSTOM};
};
std::string path{};
resource::Texture texture{};
std::map<int, Region> regions{};
std::vector<int> regionOrder{};
};
struct Sound
{
std::string path{};
resource::Audio audio{};
};
struct Layer
{
std::string name{"New Layer"};
int spritesheetID{-1};
};
struct Null
{
std::string name{"New Null"};
bool isShowRect{};
};
struct Event
{
std::string name{"New Event"};
};
struct Frame
{
glm::vec2 crop{};
glm::vec2 position{};
glm::vec2 pivot{};
glm::vec2 size{};
glm::vec2 scale{100, 100};
float rotation{};
int duration{};
glm::vec4 tint{1.0f, 1.0f, 1.0f, 1.0f};
glm::vec3 colorOffset{};
bool isInterpolated{};
int eventID{-1};
int regionID{-1};
std::vector<int> soundIDs{};
int atFrame{-1};
bool isVisible{true};
};
struct FrameOptional
{
std::optional<glm::vec2> crop{};
std::optional<glm::vec2> position{};
std::optional<glm::vec2> pivot{};
std::optional<glm::vec2> size{};
std::optional<glm::vec2> scale{};
std::optional<float> rotation{};
std::optional<glm::vec4> tint{};
std::optional<glm::vec3> colorOffset{};
std::optional<bool> isInterpolated{};
std::optional<bool> isVisible{};
};
struct Item
{
std::vector<Frame> frames{};
bool isVisible{};
};
struct Animation
{
std::string name{"New Animation"};
int frameNum{};
bool isLoop{};
Item rootAnimation{};
std::unordered_map<int, Item> layerAnimations{};
std::vector<int> layerOrder{};
std::map<int, Item> nullAnimations{};
Item triggers{};
};
int fps{30};
std::map<int, Spritesheet> spritesheets{};
std::map<int, Layer> layers{};
std::map<int, Null> nulls{};
std::map<int, Event> events{};
std::map<int, Sound> sounds{};
std::unordered_map<std::string, int> layerMap{};
std::unordered_map<std::string, int> nullMap{};
std::unordered_map<std::string, int> eventMap{};
std::string defaultAnimation{};
int defaultAnimationID{-1};
std::vector<Animation> animations{};
std::unordered_map<std::string, int> animationMap{};
std::unordered_map<int, std::string> animationMapReverse{};
std::string path{};
bool isValid{};
Flags flags{};
Anm2() = default;
Anm2(const Anm2&);
Anm2(Anm2&&);
Anm2& operator=(const Anm2&);
Anm2& operator=(Anm2&&);
Anm2(const std::filesystem::path&, Flags = 0);
Anm2(const util::physfs::Path&, Flags = 0);
bool is_valid() const;
private:
void init(tinyxml2::XMLDocument& document, Flags anm2Flags, const util::physfs::Path& archive = {});
};
}

46
src/resource/xml/area.cpp Normal file
View File

@@ -0,0 +1,46 @@
#include "area.hpp"
#include "../../log.hpp"
#include <format>
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Area::Area(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
for (auto child = root->FirstChildElement("Area"); child; child = child->NextSiblingElement("Area"))
{
Entry area{};
query_texture(child, "Texture", archive, textureRootPath, area.texture);
child->QueryFloatAttribute("Gravity", &area.gravity);
child->QueryFloatAttribute("Friction", &area.friction);
areas.emplace_back(std::move(area));
}
}
if (areas.empty()) areas.emplace_back(Entry());
isValid = true;
logger.info(std::format("Initialized area schema: {}", path.c_str()));
}
bool Area::is_valid() const { return isValid; };
}

29
src/resource/xml/area.hpp Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <vector>
#include "../texture.hpp"
#include "../../util/physfs.hpp"
namespace game::resource::xml
{
class Area
{
public:
struct Entry
{
Texture texture{};
float gravity{0.95f};
float friction{0.80f};
float airResistance{0.975f};
};
std::vector<Entry> areas{};
bool isValid{};
Area() = default;
Area(const util::physfs::Path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,241 @@
#include "character.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#include "../../util/preferences.hpp"
#include "util.hpp"
#include <format>
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Character::Character(const std::filesystem::path& path)
{
XMLDocument document;
physfs::Archive archive(path, path.filename().string());
if (!archive.is_valid())
{
logger.error(std::format("Failed to initialize character from PhysicsFS archive: {} ({})", path.string(),
physfs::error_get()));
return;
}
physfs::Path characterPath(archive + "/" + "character.xml");
if (document_load(characterPath, document) != XML_SUCCESS) return;
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
query_anm2(root, "Anm2", archive, textureRootPath, anm2);
query_string_attribute(root, "Name", &name);
query_vec3(root, "ColorR", "ColorG", "ColorB", color);
root->QueryFloatAttribute("Weight", &weight);
root->QueryFloatAttribute("Capacity", &capacity);
root->QueryFloatAttribute("CapacityMin", &capacityMin);
root->QueryFloatAttribute("CapacityMax", &capacityMax);
root->QueryFloatAttribute("CapacityMaxMultiplier", &capacityMaxMultiplier);
root->QueryFloatAttribute("CapacityIfOverStuffedOnDigestBonus", &capacityIfOverStuffedOnDigestBonus);
root->QueryFloatAttribute("CaloriesToKilogram", &caloriesToKilogram);
root->QueryFloatAttribute("DigestionRate", &digestionRate);
root->QueryFloatAttribute("DigestionRateMin", &digestionRateMin);
root->QueryFloatAttribute("DigestionRateMax", &digestionRateMax);
root->QueryIntAttribute("DigestionTimerMax", &digestionTimerMax);
root->QueryFloatAttribute("EatSpeed", &eatSpeed);
root->QueryFloatAttribute("EatSpeedMin", &eatSpeedMin);
root->QueryFloatAttribute("EatSpeedMax", &eatSpeedMax);
root->QueryFloatAttribute("BlinkChance", &blinkChance);
root->QueryFloatAttribute("GurgleChance", &gurgleChance);
root->QueryFloatAttribute("GurgleCapacityMultiplier", &gurgleCapacityMultiplier);
auto dialoguePath = physfs::Path(archive + "/" + "dialogue.xml");
if (!dialoguePath.is_valid())
logger.warning(std::format("No character dialogue.xml file found: {}", path.string()));
else
dialogue = Dialogue(dialoguePath);
dialogue.query_pool_id(root, "DialoguePoolID", pool.id);
if (auto element = root->FirstChildElement("AlternateSpritesheet"))
{
query_texture(element, "Texture", archive, textureRootPath, alternateSpritesheet.texture);
query_sound(element, "Sound", archive, soundRootPath, alternateSpritesheet.sound);
element->QueryIntAttribute("ID", &alternateSpritesheet.id);
element->QueryFloatAttribute("ChanceOnNewGame", &alternateSpritesheet.chanceOnNewGame);
}
if (auto element = root->FirstChildElement("Animations"))
{
query_animation_entry_collection(element, "FinishFood", animations.finishFood);
query_animation_entry_collection(element, "PostDigest", animations.postDigest);
if (auto child = element->FirstChildElement("Idle"))
query_string_attribute(child, "Animation", &animations.idle);
if (auto child = element->FirstChildElement("IdleFull"))
query_string_attribute(child, "Animation", &animations.idleFull);
if (auto child = element->FirstChildElement("StageUp"))
query_string_attribute(child, "Animation", &animations.stageUp);
}
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Digest", archive, soundRootPath, sounds.digest);
query_sound_entry_collection(element, "Gurgle", archive, soundRootPath, sounds.gurgle);
}
if (auto element = root->FirstChildElement("Overrides"))
{
if (auto child = element->FirstChildElement("Talk"))
{
query_layer_id(child, "LayerSource", anm2, talkOverride.layerSource);
query_layer_id(child, "LayerDestination", anm2, talkOverride.layerDestination);
}
if (auto child = element->FirstChildElement("Blink"))
{
query_layer_id(child, "LayerSource", anm2, blinkOverride.layerSource);
query_layer_id(child, "LayerDestination", anm2, blinkOverride.layerDestination);
}
}
if (auto element = root->FirstChildElement("Stages"))
{
for (auto child = element->FirstChildElement("Stage"); child; child = child->NextSiblingElement("Stage"))
{
Stage stage{};
child->QueryFloatAttribute("Threshold", &stage.threshold);
child->QueryIntAttribute("AreaID", &stage.areaID);
dialogue.query_pool_id(child, "DialoguePoolID", stage.pool.id);
stages.emplace_back(std::move(stage));
}
}
if (auto element = root->FirstChildElement("EatAreas"))
{
for (auto child = element->FirstChildElement("EatArea"); child; child = child->NextSiblingElement("EatArea"))
{
EatArea eatArea{};
query_null_id(child, "Null", anm2, eatArea.nullID);
query_event_id(child, "Event", anm2, eatArea.eventID);
query_string_attribute(child, "Animation", &eatArea.animation);
eatAreas.emplace_back(std::move(eatArea));
}
}
if (auto element = root->FirstChildElement("ExpandAreas"))
{
for (auto child = element->FirstChildElement("ExpandArea"); child;
child = child->NextSiblingElement("ExpandArea"))
{
ExpandArea expandArea{};
query_layer_id(child, "Layer", anm2, expandArea.layerID);
query_null_id(child, "Null", anm2, expandArea.nullID);
child->QueryFloatAttribute("ScaleAdd", &expandArea.scaleAdd);
expandAreas.emplace_back(std::move(expandArea));
}
}
if (auto element = root->FirstChildElement("InteractAreas"))
{
auto interact_type_id_get = [&](const std::string& typeName)
{
for (int i = 0; i < (int)interactTypeNames.size(); i++)
if (interactTypeNames[i] == typeName) return i;
interactTypeNames.emplace_back(typeName);
return (int)interactTypeNames.size() - 1;
};
for (auto child = element->FirstChildElement("InteractArea"); child;
child = child->NextSiblingElement("InteractArea"))
{
InteractArea interactArea{};
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);
query_bool_attribute(child, "IsHold", &interactArea.isHold);
child->QueryFloatAttribute("DigestionBonusRub", &interactArea.digestionBonusRub);
child->QueryFloatAttribute("DigestionBonusClick", &interactArea.digestionBonusClick);
child->QueryFloatAttribute("Time", &interactArea.time);
child->QueryFloatAttribute("ScaleEffectAmplitude", &interactArea.scaleEffectAmplitude);
child->QueryFloatAttribute("ScaleEffectCycles", &interactArea.scaleEffectCycles);
std::string typeString{};
query_string_attribute(child, "Type", &typeString);
if (!typeString.empty()) interactArea.typeID = interact_type_id_get(typeString);
interactAreas.emplace_back(std::move(interactArea));
}
}
}
if (auto itemSchemaPath = physfs::Path(archive + "/" + "items.xml"); itemSchemaPath.is_valid())
itemSchema = Item(itemSchemaPath);
else
logger.warning(std::format("No character items.xml file found: {}", path.string()));
if (auto areaSchemaPath = physfs::Path(archive + "/" + "areas.xml"); areaSchemaPath.is_valid())
areaSchema = Area(areaSchemaPath);
else
logger.warning(std::format("No character areas.xml file found: {}", path.string()));
if (auto menuSchemaPath = physfs::Path(archive + "/" + "menu.xml"); menuSchemaPath.is_valid())
menuSchema = Menu(menuSchemaPath);
else
logger.warning(std::format("No character menu.xml file found: {}", path.string()));
if (auto cursorSchemaPath = physfs::Path(archive + "/" + "cursor.xml"); cursorSchemaPath.is_valid())
cursorSchema = Cursor(cursorSchemaPath);
else
logger.warning(std::format("No character cursor.xml file found: {}", path.string()));
if (auto skillCheckSchemaPath = physfs::Path(archive + "/" + "skill_check.xml"); skillCheckSchemaPath.is_valid())
skillCheckSchema = SkillCheck(skillCheckSchemaPath, dialogue);
else
logger.warning(std::format("No character skill_check.xml file found: {}", path.string()));
logger.info(std::format("Initialized character: {}", name));
this->path = path;
save = Save(save_path_get());
}
std::filesystem::path Character::save_path_get()
{
auto savePath = path.stem();
savePath = preferences::path() / "saves" / savePath.replace_extension(".save");
std::filesystem::create_directories(savePath.parent_path());
return savePath;
}
}

View File

@@ -0,0 +1,145 @@
#pragma once
#include <filesystem>
#include <vector>
#include "../audio.hpp"
#include "animation_entry.hpp"
#include "anm2.hpp"
#include "area.hpp"
#include "cursor.hpp"
#include "dialogue.hpp"
#include "item.hpp"
#include "menu.hpp"
#include "skill_check.hpp"
#include "save.hpp"
namespace game::resource::xml
{
class Character
{
public:
struct Stage
{
float threshold{};
int areaID{};
Dialogue::PoolReference pool{-1};
};
struct EatArea
{
int nullID{-1};
int eventID{-1};
std::string animation{};
};
struct ExpandArea
{
int layerID{-1};
int nullID{-1};
float scaleAdd{};
};
struct InteractArea
{
std::string animation{};
std::string animationFull{};
std::string animationCursorActive{};
std::string animationCursorHover{};
SoundEntryCollection sound{};
int nullID{-1};
int layerID{-1};
int typeID{-1};
bool isHold{};
Dialogue::PoolReference pool{-1};
float digestionBonusRub{};
float digestionBonusClick{};
float time{};
float scaleEffectAmplitude{};
float scaleEffectCycles{};
};
struct Animations
{
AnimationEntryCollection finishFood{};
AnimationEntryCollection postDigest{};
std::string idle{};
std::string idleFull{};
std::string stageUp{};
};
struct Sounds
{
SoundEntryCollection gurgle{};
SoundEntryCollection digest{};
};
struct Override
{
int layerSource{};
int layerDestination{};
};
struct AlternateSpritesheet
{
Texture texture{};
Audio sound{};
int id{-1};
float chanceOnNewGame{0.001f};
};
Anm2 anm2{};
Area areaSchema{};
Dialogue dialogue{};
Item itemSchema{};
Menu menuSchema{};
Cursor cursorSchema{};
SkillCheck skillCheckSchema{};
Save save{};
Animations animations{};
Override talkOverride{};
Override blinkOverride{};
Sounds sounds{};
glm::vec3 color{0.120f, 0.515f, 0.115f};
std::vector<Stage> stages{};
std::vector<ExpandArea> expandAreas{};
std::vector<EatArea> eatAreas{};
std::vector<std::string> interactTypeNames{};
std::vector<InteractArea> interactAreas{};
AlternateSpritesheet alternateSpritesheet{};
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};
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};
Dialogue::PoolReference pool{-1};
Character() = default;
Character(const std::filesystem::path&);
std::filesystem::path save_path_get();
};
}

View File

@@ -0,0 +1,69 @@
#include "character_preview.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#include "../../util/preferences.hpp"
#include "util.hpp"
#include <format>
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
CharacterPreview::CharacterPreview(const std::filesystem::path& path)
{
XMLDocument document;
physfs::Archive archive(path, path.filename().string());
if (!archive.is_valid())
{
logger.error(std::format("Failed to initialize character preview from PhysicsFS archive: {} ({})", path.string(),
physfs::error_get()));
return;
}
physfs::Path characterPath(archive + "/" + "character.xml");
if (document_load(characterPath, document) != XML_SUCCESS) return;
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
query_anm2(root, "Anm2", archive, textureRootPath, anm2, Anm2::NO_SOUNDS | Anm2::DEFAULT_ANIMATION_ONLY);
query_texture(root, "Render", archive, textureRootPath, render);
query_texture(root, "Portrait", archive, textureRootPath, portrait);
query_string_attribute(root, "Name", &name);
query_string_attribute(root, "Description", &description);
query_string_attribute(root, "Author", &author);
query_vec3(root, "ColorR", "ColorG", "ColorB", color);
root->QueryFloatAttribute("Weight", &weight);
if (auto element = root->FirstChildElement("Stages"))
for (auto child = element->FirstChildElement("Stage"); child; child = child->NextSiblingElement("Stage"))
stages++;
}
this->path = path;
save = Save(save_path_get());
isValid = true;
logger.info(std::format("Initialized character preview: {}", name));
}
std::filesystem::path CharacterPreview::save_path_get()
{
auto savePath = path.stem();
savePath = preferences::path() / "saves" / savePath.replace_extension(".save");
std::filesystem::create_directories(savePath.parent_path());
return savePath;
}
bool CharacterPreview::is_valid() const { return isValid; }
}

View File

@@ -0,0 +1,44 @@
#pragma once
#include <filesystem>
#include <glm/glm.hpp>
#include <string>
#include <vector>
#include "anm2.hpp"
#include "save.hpp"
namespace game::resource::xml
{
class CharacterPreview
{
public:
struct Stage
{
float threshold{};
int dialoguePoolID{-1};
};
Anm2 anm2{};
Texture portrait{};
Texture render{};
Save save{};
glm::vec3 color{0.120f, 0.515f, 0.115f};
int stages{1};
std::string name{};
std::string author{};
std::string description{};
std::filesystem::path path{};
float weight{50};
bool isValid{};
CharacterPreview() = default;
CharacterPreview(const std::filesystem::path&);
std::filesystem::path save_path_get();
bool is_valid() const;
};
}

View File

@@ -0,0 +1,54 @@
#include "cursor.hpp"
#include "../../log.hpp"
#include <format>
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Cursor::Cursor(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
query_anm2(root, "Anm2", archive, textureRootPath, anm2);
if (auto element = root->FirstChildElement("Animations"))
{
query_animation_entry_collection(element, "Idle", animations.idle);
query_animation_entry_collection(element, "Hover", animations.hover);
query_animation_entry_collection(element, "Grab", animations.grab);
query_animation_entry_collection(element, "Pan", animations.pan);
query_animation_entry_collection(element, "Zoom", animations.zoom);
query_animation_entry_collection(element, "Return", animations.return_);
}
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Grab", archive, soundRootPath, sounds.grab);
query_sound_entry_collection(element, "Release", archive, soundRootPath, sounds.release);
query_sound_entry_collection(element, "Throw", archive, soundRootPath, sounds.throw_);
}
}
isValid = true;
logger.info(std::format("Initialized area schema: {}", path.c_str()));
}
bool Cursor::is_valid() const { return isValid; };
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include "util.hpp"
namespace game::resource::xml
{
class Cursor
{
public:
struct Animations
{
AnimationEntryCollection idle{};
AnimationEntryCollection hover{};
AnimationEntryCollection grab{};
AnimationEntryCollection pan{};
AnimationEntryCollection zoom{};
AnimationEntryCollection return_{};
};
struct Sounds
{
SoundEntryCollection grab{};
SoundEntryCollection release{};
SoundEntryCollection throw_{};
};
Animations animations{};
Sounds sounds{};
Anm2 anm2{};
bool isValid{};
Cursor() = default;
Cursor(const util::physfs::Path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,172 @@
#include "dialogue.hpp"
#include "util.hpp"
#include "../../log.hpp"
#include "../../util/math.hpp"
#include <format>
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
void Dialogue::query_entry_id(XMLElement* element, const char* name, int& id)
{
std::string entryID{};
query_string_attribute(element, name, &entryID);
if (entryIDMap.contains(entryID))
id = entryIDMap.at(entryID);
else if (entryID.empty())
entryID = -1;
else
{
logger.warning("Dialogue entries does not contain: " + entryID);
id = -1;
}
}
void Dialogue::query_pool_id(XMLElement* element, const char* name, int& id)
{
std::string poolID{};
query_string_attribute(element, name, &poolID);
if (poolMap.contains(poolID))
id = poolMap.at(poolID);
else if (poolID.empty())
poolID = -1;
else
{
logger.warning("Dialogue pools does not contain: " + poolID);
id = -1;
}
}
Dialogue::Dialogue(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
if (auto root = document.RootElement())
{
if (auto element = root->FirstChildElement("Entries"))
{
int id{};
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
{
std::string stringID{};
query_string_attribute(child, "ID", &stringID);
entryIDMap.emplace(stringID, id);
entryIDMapReverse.emplace(id, stringID);
id++;
}
id = 0;
for (auto child = element->FirstChildElement("Entry"); child; child = child->NextSiblingElement("Entry"))
{
Entry entry{};
entry.name = entryIDMapReverse.at(id);
query_string_attribute(child, "Text", &entry.text);
query_string_attribute(child, "Animation", &entry.animation);
if (child->FindAttribute("Next"))
{
std::string nextID{};
query_string_attribute(child, "Next", &nextID);
if (!entryIDMap.contains(nextID))
logger.warning(std::format("Dialogue: next ID does not point to a valid Entry! ({})", nextID));
else
entry.nextID = entryIDMap.at(nextID);
}
for (auto choiceChild = child->FirstChildElement("Choice"); choiceChild;
choiceChild = choiceChild->NextSiblingElement("Choice"))
{
Choice choice{};
query_entry_id(choiceChild, "Next", choice.nextID);
query_string_attribute(choiceChild, "Text", &choice.text);
entry.choices.emplace_back(std::move(choice));
}
entries.emplace_back(std::move(entry));
id++;
}
}
if (auto element = root->FirstChildElement("Pools"))
{
int id{};
for (auto child = element->FirstChildElement("Pool"); child; child = child->NextSiblingElement("Pool"))
{
Pool pool{};
std::string stringID{};
query_string_attribute(child, "ID", &stringID);
poolMap[stringID] = id;
pools.emplace_back(pool);
id++;
}
id = 0;
for (auto child = element->FirstChildElement("Pool"); child; child = child->NextSiblingElement("Pool"))
{
auto& pool = pools.at(id);
for (auto entryChild = child->FirstChildElement("PoolEntry"); entryChild;
entryChild = entryChild->NextSiblingElement("PoolEntry"))
{
int entryID{};
query_entry_id(entryChild, "ID", entryID);
pool.emplace_back(entryID);
}
id++;
}
logger.info(std::format("Initialized dialogue: {}", path.c_str()));
isValid = true;
}
if (auto element = root->FirstChildElement("Start"))
{
query_entry_id(element, "ID", start.id);
query_string_attribute(element, "Animation", &start.animation);
}
if (auto element = root->FirstChildElement("End")) query_entry_id(element, "ID", end.id);
if (auto element = root->FirstChildElement("Help")) query_entry_id(element, "ID", help.id);
if (auto element = root->FirstChildElement("Digest")) query_pool_id(element, "PoolID", digest.id);
if (auto element = root->FirstChildElement("Eat")) query_pool_id(element, "PoolID", eat.id);
if (auto element = root->FirstChildElement("EatFull")) query_pool_id(element, "PoolID", eatFull.id);
if (auto element = root->FirstChildElement("Feed")) query_pool_id(element, "PoolID", feed.id);
if (auto element = root->FirstChildElement("FeedFull")) query_pool_id(element, "PoolID", feedFull.id);
if (auto element = root->FirstChildElement("FoodTaken")) query_pool_id(element, "PoolID", foodTaken.id);
if (auto element = root->FirstChildElement("FoodTakenFull")) query_pool_id(element, "PoolID", foodTakenFull.id);
if (auto element = root->FirstChildElement("Full")) query_pool_id(element, "PoolID", full.id);
if (auto element = root->FirstChildElement("LowCapacity")) query_pool_id(element, "PoolID", lowCapacity.id);
if (auto element = root->FirstChildElement("Random")) query_pool_id(element, "PoolID", random.id);
if (auto element = root->FirstChildElement("Throw")) query_pool_id(element, "PoolID", throw_.id);
if (auto element = root->FirstChildElement("StageUp")) query_pool_id(element, "PoolID", stageUp.id);
}
}
int Dialogue::Pool::get() const
{
if (this->empty()) return -1;
auto index = rand() % this->size();
return this->at(index);
}
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()); }
}

View File

@@ -0,0 +1,90 @@
#pragma once
#include <tinyxml2.h>
#include <string>
#include <map>
#include "../../util/physfs.hpp"
namespace game::resource::xml
{
class Dialogue
{
public:
struct Choice
{
std::string text{};
int nextID{-1};
};
struct Entry
{
std::string name{};
std::string animation{};
std::string text{};
std::vector<Choice> choices{};
int nextID{-1};
inline bool is_last() const { return choices.empty() && nextID == -1; };
};
struct EntryReference
{
int id{-1};
std::string animation{};
inline bool is_valid() const { return id != -1; };
};
class PoolReference
{
public:
int id{-1};
inline bool is_valid() const { return id != -1; };
};
class Pool : public std::vector<int>
{
public:
int get() const;
};
std::map<std::string, int> entryIDMap;
std::map<int, std::string> entryIDMapReverse;
std::vector<Entry> entries{};
std::vector<Pool> pools{};
std::map<std::string, int> poolMap{};
EntryReference start{-1};
EntryReference end{-1};
EntryReference help{-1};
PoolReference digest{-1};
PoolReference eatFull{-1};
PoolReference eat{-1};
PoolReference feedFull{-1};
PoolReference feed{-1};
PoolReference foodTakenFull{-1};
PoolReference foodTaken{-1};
PoolReference full{-1};
PoolReference random{-1};
PoolReference lowCapacity{-1};
PoolReference throw_{-1};
PoolReference stageUp{-1};
bool isValid{};
Dialogue() = default;
Dialogue(const util::physfs::Path&);
Entry* get(const std::string&);
Entry* get(int id);
Entry* get(Dialogue::EntryReference&);
Entry* get(Dialogue::Pool&);
Entry* get(Dialogue::PoolReference&);
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; };
};
}

191
src/resource/xml/item.cpp Normal file
View File

@@ -0,0 +1,191 @@
#include "item.hpp"
#include <ranges>
#include <tinyxml2.h>
#include <algorithm>
#include <utility>
#include "../../log.hpp"
#include <format>
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Item::Item(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string textureRootPath{};
query_string_attribute(root, "TextureRootPath", &textureRootPath);
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
query_anm2(root, "BaseAnm2", archive, textureRootPath, baseAnm2, Anm2::NO_SPRITESHEETS);
if (auto element = root->FirstChildElement("Categories"))
{
for (auto child = element->FirstChildElement("Category"); child; child = child->NextSiblingElement("Category"))
{
Category category{};
query_string_attribute(child, "Name", &category.name);
query_bool_attribute(child, "IsEdible", &category.isEdible);
categoryMap[category.name] = (int)categories.size();
categories.push_back(category);
}
}
if (auto element = root->FirstChildElement("Rarities"))
{
for (auto child = element->FirstChildElement("Rarity"); child; child = child->NextSiblingElement("Rarity"))
{
Rarity rarity{};
query_string_attribute(child, "Name", &rarity.name);
child->QueryFloatAttribute("Chance", &rarity.chance);
query_bool_attribute(child, "IsHidden", &rarity.isHidden);
query_sound(child, "Sound", archive, soundRootPath, rarity.sound);
rarityMap[rarity.name] = (int)rarities.size();
rarities.emplace_back(std::move(rarity));
}
}
if (auto element = root->FirstChildElement("Flavors"))
{
for (auto child = element->FirstChildElement("Flavor"); child; child = child->NextSiblingElement("Flavor"))
{
Flavor flavor{};
query_string_attribute(child, "Name", &flavor.name);
flavorMap[flavor.name] = (int)flavors.size();
flavors.push_back(flavor);
}
}
if (auto element = root->FirstChildElement("Animations"))
{
if (auto child = element->FirstChildElement("Chew"))
query_string_attribute(child, "Animation", &animations.chew);
}
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Bounce", archive, soundRootPath, sounds.bounce);
query_sound_entry_collection(element, "Dispose", archive, soundRootPath, sounds.dispose);
query_sound_entry_collection(element, "Return", archive, soundRootPath, sounds.return_);
query_sound_entry_collection(element, "Summon", archive, soundRootPath, sounds.summon);
query_sound_entry_collection(element, "Upgrade", archive, soundRootPath, sounds.upgrade);
query_sound_entry_collection(element, "UpgradeFail", archive, soundRootPath, sounds.upgradeFail);
}
if (auto element = root->FirstChildElement("Items"))
{
int id{};
for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item"))
{
std::string name{};
query_string_attribute(child, "Name", &name);
stringToIDMap[name] = id;
idToStringMap[id] = name;
id++;
}
}
if (auto element = root->FirstChildElement("Items"))
{
std::string itemTextureRootPath{};
query_string_attribute(element, "TextureRootPath", &itemTextureRootPath);
element->QueryIntAttribute("ChewCount", &chewCount);
element->QueryIntAttribute("QuantityMax", &quantityMax);
for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item"))
{
Entry item{};
query_string_attribute(child, "Name", &item.name);
query_string_attribute(child, "Description", &item.description);
query_float_optional_attribute(child, "Calories", item.calories);
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);
if (child->FindAttribute("UpgradeID"))
{
std::string upgradeIDString{};
query_string_attribute(child, "UpgradeID", &upgradeIDString);
if (!upgradeIDString.empty() && stringToIDMap.contains(upgradeIDString))
item.upgradeID = stringToIDMap[upgradeIDString];
else if (upgradeIDString.empty())
logger.warning(std::format("Empty UpgradeID ({})", item.name));
else
logger.warning(std::format("Could not find item ID for UpgradeID: {} ({})", upgradeIDString, item.name));
query_int_optional_attribute(child, "UpgradeCount", item.upgradeCount);
}
query_bool_attribute(child, "IsSkillCheckReward", &item.isSkillCheckReward);
query_bool_attribute(child, "IsToggleSpritesheet", &item.isToggleSpritesheet);
std::string categoryString{};
query_string_attribute(child, "Category", &categoryString);
item.categoryID = categoryMap.contains(categoryString) ? categoryMap[categoryString] : -1;
std::string rarityString{};
query_string_attribute(child, "Rarity", &rarityString);
item.rarityID = rarityMap.contains(rarityString) ? rarityMap[rarityString] : -1;
std::string flavorString{};
query_string_attribute(child, "Flavor", &flavorString);
if (flavorMap.contains(flavorString)) item.flavorID = flavorMap[flavorString];
Texture texture{};
query_texture(child, "Texture", archive, itemTextureRootPath, texture);
Anm2 anm2{baseAnm2};
if (child->FindAttribute("Anm2")) query_anm2(child, "Anm2", archive, textureRootPath, anm2);
anm2.spritesheets.at(0).texture = std::move(texture);
anm2s.emplace_back(std::move(anm2));
items.emplace_back(std::move(item));
}
}
}
for (int i = 0; i < (int)items.size(); i++)
{
auto& item = items[i];
pools[item.rarityID].emplace_back(i);
if (item.isSkillCheckReward) skillCheckRewardItemPool.emplace_back(i);
}
for (int i = 0; i < (int)rarities.size(); i++)
{
rarityIDsSortedByChance.emplace_back(i);
}
std::stable_sort(rarityIDsSortedByChance.begin(), rarityIDsSortedByChance.end(),
[&](int a, int b) { return rarities[a].chance > rarities[b].chance; });
isValid = true;
logger.info(std::format("Initialized item schema: {}", path.c_str()));
}
bool Item::is_valid() const { return isValid; };
}

104
src/resource/xml/item.hpp Normal file
View File

@@ -0,0 +1,104 @@
#pragma once
#include <filesystem>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "../audio.hpp"
#include "anm2.hpp"
#include "sound_entry.hpp"
namespace game::resource::xml
{
class Item
{
public:
static constexpr auto UNDEFINED = "???";
struct Category
{
std::string name{};
bool isEdible{};
};
struct Rarity
{
std::string name{UNDEFINED};
float chance{};
bool isHidden{};
Audio sound{};
};
struct Flavor
{
std::string name{UNDEFINED};
};
struct Entry
{
std::string name{UNDEFINED};
std::string description{UNDEFINED};
int categoryID{};
int rarityID{};
std::optional<int> upgradeCount{};
std::optional<int> upgradeID{};
std::optional<int> flavorID;
std::optional<float> calories{};
std::optional<float> eatSpeedBonus{};
std::optional<float> digestionBonus{};
std::optional<float> gravity{};
std::optional<int> chewCount{};
bool isSkillCheckReward{};
bool isToggleSpritesheet{};
};
struct Animations
{
std::string chew{};
};
struct Sounds
{
SoundEntryCollection bounce{};
SoundEntryCollection return_{};
SoundEntryCollection dispose{};
SoundEntryCollection summon{};
SoundEntryCollection upgrade{};
SoundEntryCollection upgradeFail{};
};
std::unordered_map<std::string, int> categoryMap{};
std::unordered_map<std::string, int> rarityMap{};
std::unordered_map<std::string, int> flavorMap{};
std::unordered_map<std::string, int> stringToIDMap{};
std::unordered_map<int, std::string> idToStringMap{};
using Pool = std::vector<int>;
std::vector<Category> categories{};
std::vector<Rarity> rarities{};
std::vector<Flavor> flavors{};
std::vector<Entry> items{};
std::vector<Anm2> anm2s{};
std::vector<int> rarityIDsSortedByChance{};
std::unordered_map<int, Pool> pools{};
Pool skillCheckRewardItemPool{};
Animations animations{};
Sounds sounds{};
Anm2 baseAnm2{};
int chewCount{2};
int quantityMax{99};
bool isValid{};
Item() = default;
Item(const std::filesystem::path&);
Item(const util::physfs::Path&);
bool is_valid() const;
};
}

49
src/resource/xml/menu.cpp Normal file
View File

@@ -0,0 +1,49 @@
#include "menu.hpp"
#include "../../log.hpp"
#include <format>
#include "util.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Menu::Menu(const physfs::Path& path)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
std::string fontRootPath{};
query_string_attribute(root, "FontRootPath", &fontRootPath);
query_font(root, "Font", archive, fontRootPath, font);
root->QueryFloatAttribute("Rounding", &rounding);
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Open", archive, soundRootPath, sounds.open);
query_sound_entry_collection(element, "Close", archive, soundRootPath, sounds.close);
query_sound_entry_collection(element, "Hover", archive, soundRootPath, sounds.hover);
query_sound_entry_collection(element, "Select", archive, soundRootPath, sounds.select);
query_sound_entry_collection(element, "CheatsActivated", archive, soundRootPath, sounds.cheatsActivated);
}
}
isValid = true;
logger.info(std::format("Initialized menu schema: {}", path.c_str()));
}
bool Menu::is_valid() const { return isValid; };
}

32
src/resource/xml/menu.hpp Normal file
View File

@@ -0,0 +1,32 @@
#pragma once
#include "../../util/physfs.hpp"
#include "../font.hpp"
#include "sound_entry.hpp"
namespace game::resource::xml
{
class Menu
{
public:
struct Sounds
{
SoundEntryCollection open{};
SoundEntryCollection close{};
SoundEntryCollection hover{};
SoundEntryCollection select{};
SoundEntryCollection cheatsActivated{};
};
Sounds sounds{};
Font font{};
float rounding{};
bool isValid{};
Menu() = default;
Menu(const util::physfs::Path&);
bool is_valid() const;
};
}

187
src/resource/xml/save.cpp Normal file
View File

@@ -0,0 +1,187 @@
#include "save.hpp"
#include "util.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#include <format>
#ifdef __EMSCRIPTEN__
#include "../../util/web_filesystem.hpp"
#endif
using namespace game::util;
using namespace tinyxml2;
namespace game::resource::xml
{
Save::Save(const std::filesystem::path& path)
{
XMLDocument document;
auto pathString = path.string();
auto result = document.LoadFile(pathString.c_str());
if (result == XML_ERROR_FILE_NOT_FOUND || result == XML_ERROR_FILE_COULD_NOT_BE_OPENED) return;
if (result != XML_SUCCESS)
{
logger.error(std::format("Could not initialize character save file: {} ({})", pathString, document.ErrorStr()));
return;
}
if (auto root = document.RootElement())
{
query_bool_attribute(root, "IsPostgame", &isPostgame);
query_bool_attribute(root, "IsAlternateSpritesheet", &isAlternateSpritesheet);
if (auto element = root->FirstChildElement("Character"))
{
element->QueryFloatAttribute("Weight", &weight);
element->QueryFloatAttribute("Calories", &calories);
element->QueryFloatAttribute("Capacity", &capacity);
element->QueryFloatAttribute("DigestionRate", &digestionRate);
element->QueryFloatAttribute("EatSpeed", &eatSpeed);
query_bool_attribute(element, "IsDigesting", &isDigesting);
element->QueryFloatAttribute("DigestionProgress", &digestionProgress);
element->QueryIntAttribute("DigestionTimer", &digestionTimer);
element->QueryFloatAttribute("TotalCaloriesConsumed", &totalCaloriesConsumed);
element->QueryIntAttribute("TotalFoodItemsEaten", &totalFoodItemsEaten);
}
auto element = root->FirstChildElement("SkillCheck");
if (!element) element = root->FirstChildElement("Play");
if (element)
{
element->QueryIntAttribute("TotalPlays", &totalPlays);
element->QueryIntAttribute("HighScore", &highScore);
element->QueryIntAttribute("BestCombo", &bestCombo);
if (auto child = element->FirstChildElement("Grades"))
{
for (auto gradeChild = child->FirstChildElement("Grade"); gradeChild;
gradeChild = gradeChild->NextSiblingElement("Grade"))
{
int id{};
gradeChild->QueryIntAttribute("ID", &id);
gradeChild->QueryIntAttribute("Count", &gradeCounts[id]);
}
}
}
if (auto element = root->FirstChildElement("Inventory"))
{
for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item"))
{
int id{};
int quantity{};
child->QueryIntAttribute("ID", &id);
child->QueryIntAttribute("Quantity", &quantity);
inventory[id] = quantity;
}
}
if (auto element = root->FirstChildElement("Items"))
{
for (auto child = element->FirstChildElement("Item"); child; child = child->NextSiblingElement("Item"))
{
Item item{};
child->QueryIntAttribute("ID", &item.id);
child->QueryIntAttribute("ChewCount", &item.chewCount);
child->QueryFloatAttribute("PositionX", &item.position.x);
child->QueryFloatAttribute("PositionY", &item.position.y);
child->QueryFloatAttribute("VelocityX", &item.velocity.x);
child->QueryFloatAttribute("VelocityY", &item.velocity.y);
child->QueryFloatAttribute("Rotation", &item.rotation);
items.emplace_back(std::move(item));
}
}
}
logger.info(std::format("Initialized character save file: {}", pathString));
isValid = true;
}
bool Save::is_valid() const { return isValid; }
void Save::serialize(const std::filesystem::path& path)
{
XMLDocument document;
auto pathString = path.string();
auto element = document.NewElement("Save");
element->SetAttribute("IsPostgame", isPostgame ? "true" : "false");
element->SetAttribute("IsAlternateSpritesheet", isAlternateSpritesheet ? "true" : "false");
auto characterElement = element->InsertNewChildElement("Character");
characterElement->SetAttribute("Weight", weight);
characterElement->SetAttribute("Calories", calories);
characterElement->SetAttribute("Capacity", capacity);
characterElement->SetAttribute("DigestionRate", digestionRate);
characterElement->SetAttribute("EatSpeed", eatSpeed);
characterElement->SetAttribute("IsDigesting", isDigesting ? "true" : "false");
characterElement->SetAttribute("DigestionProgress", digestionProgress);
characterElement->SetAttribute("DigestionTimer", digestionTimer);
characterElement->SetAttribute("TotalCaloriesConsumed", totalCaloriesConsumed);
characterElement->SetAttribute("TotalFoodItemsEaten", totalFoodItemsEaten);
auto skillCheckElement = element->InsertNewChildElement("SkillCheck");
skillCheckElement->SetAttribute("TotalPlays", totalPlays);
skillCheckElement->SetAttribute("HighScore", highScore);
skillCheckElement->SetAttribute("BestCombo", bestCombo);
auto gradesElement = skillCheckElement->InsertNewChildElement("Grades");
for (auto& [i, count] : gradeCounts)
{
auto gradeElement = gradesElement->InsertNewChildElement("Grade");
gradeElement->SetAttribute("ID", i);
gradeElement->SetAttribute("Count", count);
}
auto inventoryElement = element->InsertNewChildElement("Inventory");
for (auto& [id, quantity] : inventory)
{
auto itemElement = inventoryElement->InsertNewChildElement("Item");
itemElement->SetAttribute("ID", id);
itemElement->SetAttribute("Quantity", quantity);
}
auto itemsElement = element->InsertNewChildElement("Items");
for (auto& item : items)
{
auto itemElement = itemsElement->InsertNewChildElement("Item");
itemElement->SetAttribute("ID", item.id);
itemElement->SetAttribute("ChewCount", item.chewCount);
itemElement->SetAttribute("PositionX", item.position.x);
itemElement->SetAttribute("PositionY", item.position.y);
itemElement->SetAttribute("VelocityX", item.velocity.x);
itemElement->SetAttribute("VelocityY", item.velocity.y);
itemElement->SetAttribute("Rotation", item.rotation);
}
document.InsertFirstChild(element);
if (document.SaveFile(pathString.c_str()) != XML_SUCCESS)
{
logger.error(std::format("Failed to save character save file: {} ({})", pathString, document.ErrorStr()));
return;
}
logger.info(std::format("Saved character save file: {}", pathString));
#ifdef __EMSCRIPTEN__
web_filesystem::flush_async();
#endif
}
}

54
src/resource/xml/save.hpp Normal file
View File

@@ -0,0 +1,54 @@
#pragma once
#include <filesystem>
#include <map>
#include <vector>
#include <glm/glm.hpp>
namespace game::resource::xml
{
class Save
{
public:
struct Item
{
int id{};
int chewCount{};
glm::vec2 position{};
glm::vec2 velocity{};
float rotation{};
};
float weight{};
float calories{};
float capacity{};
float eatSpeed{};
float digestionRate{};
float digestionProgress{};
int digestionTimer{};
bool isDigesting{};
bool isAlternateSpritesheet{};
float totalCaloriesConsumed{};
int totalFoodItemsEaten{};
int totalPlays{};
int highScore{};
int bestCombo{};
std::map<int, int> gradeCounts{};
std::map<int, int> inventory;
std::vector<Item> items;
bool isPostgame{};
bool isValid{};
Save() = default;
Save(const std::filesystem::path&);
void serialize(const std::filesystem::path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,77 @@
#include "settings.hpp"
#include "util.hpp"
#include <tinyxml2/tinyxml2.h>
#include "../../log.hpp"
#include <format>
#ifdef __EMSCRIPTEN__
#include "../../util/web_filesystem.hpp"
#endif
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
Settings::Settings(const std::filesystem::path& path)
{
XMLDocument document;
auto pathString = path.string();
if (document.LoadFile(pathString.c_str()) != XML_SUCCESS)
{
logger.error(std::format("Could not initialize character save file: {} ({})", pathString, document.ErrorStr()));
return;
}
if (auto root = document.RootElement())
{
std::string measurementSystemString{};
query_string_attribute(root, "MeasurementSystem", &measurementSystemString);
measurementSystem = measurementSystemString == "Imperial" ? measurement::IMPERIAL : measurement::METRIC;
root->QueryIntAttribute("Volume", &volume);
query_vec3(root, "ColorR", "ColorG", "ColorB", color);
query_vec2(root, "WindowX", "WindowY", windowPosition);
query_ivec2(root, "WindowW", "WindowH", windowSize);
query_bool_attribute(root, "IsUseCharacterColor", &isUseCharacterColor);
}
logger.info(std::format("Initialized settings: {}", pathString));
isValid = true;
}
bool Settings::is_valid() const { return isValid; }
void Settings::serialize(const std::filesystem::path& path)
{
XMLDocument document;
auto pathString = path.string();
auto element = document.NewElement("Settings");
element->SetAttribute("MeasurementSystem", measurementSystem == measurement::IMPERIAL ? "Imperial" : "Metric");
element->SetAttribute("Volume", volume);
set_vec3_attribute(element, "ColorR", "ColorG", "ColorB", color);
set_vec2_attribute(element, "WindowX", "WindowY", windowPosition);
set_ivec2_attribute(element, "WindowW", "WindowH", windowSize);
set_bool_attribute(element, "IsUseCharacterColor", isUseCharacterColor);
document.InsertFirstChild(element);
if (document.SaveFile(pathString.c_str()) != XML_SUCCESS)
{
logger.info(std::format("Failed to initialize settings: {} ({})", pathString, document.ErrorStr()));
return;
}
logger.info(std::format("Saved settings: {}", pathString));
#ifdef __EMSCRIPTEN__
web_filesystem::flush_async();
#endif
}
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include "../../util/measurement.hpp"
#include <filesystem>
#include <glm/glm.hpp>
namespace game::resource::xml
{
class Settings
{
public:
static constexpr auto VOLUME_MIN = 0;
static constexpr auto VOLUME_MAX = 100;
enum Mode
{
LOADER,
IMGUI
};
util::measurement::System measurementSystem{util::measurement::METRIC};
int volume{50};
bool isUseCharacterColor{true};
glm::vec3 color{0.120f, 0.515f, 0.115f};
glm::ivec2 windowSize{1600, 900};
glm::vec2 windowPosition{};
bool isValid{};
Settings() = default;
Settings(const std::filesystem::path&);
void serialize(const std::filesystem::path&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,69 @@
#include "skill_check.hpp"
#include "../../log.hpp"
#include "util.hpp"
#include <format>
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
SkillCheck::SkillCheck(const physfs::Path& path, Dialogue& dialogue)
{
XMLDocument document;
if (document_load(path, document) != XML_SUCCESS) return;
auto archive = path.directory_get();
if (auto root = document.RootElement())
{
std::string soundRootPath{};
query_string_attribute(root, "SoundRootPath", &soundRootPath);
root->QueryIntAttribute("RewardScore", &rewardScore);
root->QueryFloatAttribute("RewardScoreBonus", &rewardScoreBonus);
root->QueryFloatAttribute("RewardGradeBonus", &rewardGradeBonus);
root->QueryFloatAttribute("RangeBase", &rangeBase);
root->QueryFloatAttribute("RangeMin", &rangeMin);
root->QueryFloatAttribute("RangeScoreBonus", &rangeScoreBonus);
root->QueryFloatAttribute("SpeedMin", &speedMin);
root->QueryFloatAttribute("SpeedMax", &speedMax);
root->QueryFloatAttribute("SpeedScoreBonus", &speedScoreBonus);
root->QueryIntAttribute("EndTimerMax", &endTimerMax);
root->QueryIntAttribute("EndTimerFailureMax", &endTimerFailureMax);
if (auto element = root->FirstChildElement("Sounds"))
{
query_sound_entry_collection(element, "Fall", archive, soundRootPath, sounds.fall);
query_sound_entry_collection(element, "ScoreLoss", archive, soundRootPath, sounds.scoreLoss);
query_sound_entry_collection(element, "HighScore", archive, soundRootPath, sounds.highScore);
query_sound_entry_collection(element, "HighScoreLoss", archive, soundRootPath, sounds.highScoreLoss);
query_sound_entry_collection(element, "RewardScore", archive, soundRootPath, sounds.rewardScore);
}
if (auto element = root->FirstChildElement("Grades"))
{
for (auto child = element->FirstChildElement("Grade"); child; child = child->NextSiblingElement("Grade"))
{
Grade grade{};
query_string_attribute(child, "Name", &grade.name);
query_string_attribute(child, "NamePlural", &grade.namePlural);
child->QueryIntAttribute("Value", &grade.value);
child->QueryFloatAttribute("Weight", &grade.weight);
query_bool_attribute(child, "IsFailure", &grade.isFailure);
query_sound(child, "Sound", archive, soundRootPath, grade.sound);
dialogue.query_pool_id(child, "DialoguePoolID", grade.pool.id);
grades.emplace_back(std::move(grade));
}
}
}
isValid = true;
logger.info(std::format("Initialized skill check schema: {}", path.c_str()));
}
bool SkillCheck::is_valid() const { return isValid; };
}

View File

@@ -0,0 +1,56 @@
#pragma once
#include <string>
#include "util.hpp"
#include "dialogue.hpp"
namespace game::resource::xml
{
class SkillCheck
{
public:
struct Grade
{
std::string name{};
std::string namePlural{};
int value{};
float weight{};
bool isFailure{};
Audio sound{};
Dialogue::PoolReference pool{};
};
struct Sounds
{
SoundEntryCollection fall{};
SoundEntryCollection highScore{};
SoundEntryCollection highScoreLoss{};
SoundEntryCollection rewardScore{};
SoundEntryCollection scoreLoss{};
};
Sounds sounds{};
std::vector<Grade> grades{};
float rewardScoreBonus{0.01f};
float rewardGradeBonus{0.05f};
float speedMin{0.005f};
float speedMax{0.075f};
float speedScoreBonus{0.000025f};
float rangeBase{0.75f};
float rangeMin{0.10f};
float rangeScoreBonus{0.0005f};
int endTimerMax{20};
int endTimerFailureMax{60};
int rewardScore{999};
bool isValid{};
SkillCheck() = default;
SkillCheck(const util::physfs::Path&, Dialogue&);
bool is_valid() const;
};
}

View File

@@ -0,0 +1,18 @@
#include "sound_entry.hpp"
#include "../../util/vector.hpp"
namespace game::resource::xml
{
Audio* SoundEntryCollection::get()
{
if (empty()) return nullptr;
return &at(util::vector::random_index_weighted(*this, [](const auto& entry) { return entry.weight; })).sound;
}
void SoundEntryCollection::play()
{
if (empty()) return;
if (auto audio = get()) audio->play();
}
}

View File

@@ -0,0 +1,24 @@
// Handles sound entries in .xml files. "Weight" value determines weight of being randomly selected.
#pragma once
#include "../audio.hpp"
namespace game::resource::xml
{
class SoundEntry
{
public:
Audio sound{};
float weight{1.0f};
inline void play() { sound.play(); };
};
class SoundEntryCollection : public std::vector<SoundEntry>
{
public:
Audio* get();
void play();
};
}

278
src/resource/xml/util.cpp Normal file
View File

@@ -0,0 +1,278 @@
#include "util.hpp"
#include <format>
#include "../../util/physfs.hpp"
#include "../../util/string.hpp"
#include "../../log.hpp"
using namespace tinyxml2;
using namespace game::util;
namespace game::resource::xml
{
XMLError query_result_merge(XMLError result, XMLError next) { return result == XML_SUCCESS ? next : result; }
XMLError query_string_attribute(XMLElement* element, const char* attribute, std::string* value)
{
const char* temp = nullptr;
auto result = element->QueryStringAttribute(attribute, &temp);
if (result == XML_SUCCESS && temp && value) *value = temp;
return result;
}
XMLError query_bool_attribute(XMLElement* element, const char* attribute, bool* value)
{
std::string temp{};
auto result = query_string_attribute(element, attribute, &temp);
temp = string::to_lower(temp);
if (value) *value = temp == "true" || temp == "1" ? true : false;
return result;
}
XMLError query_path_attribute(XMLElement* element, const char* attribute, std::filesystem::path* value)
{
std::string temp{};
auto result = query_string_attribute(element, attribute, &temp);
if (value) *value = std::filesystem::path(temp);
return result;
}
XMLError query_color_attribute(XMLElement* element, const char* attribute, float* value)
{
int temp{};
auto result = element->QueryIntAttribute(attribute, &temp);
if (result == XML_SUCCESS && value) *value = (temp / 255.0f);
return result;
}
XMLError query_ivec2(XMLElement* element, const char* attributeX, const char* attributeY, glm::ivec2& value)
{
auto result = element->QueryIntAttribute(attributeX, &value.x);
result = query_result_merge(result, element->QueryIntAttribute(attributeY, &value.y));
return result;
}
XMLError query_vec2(XMLElement* element, const char* attributeX, const char* attributeY, glm::vec2& value)
{
auto result = element->QueryFloatAttribute(attributeX, &value.x);
result = query_result_merge(result, element->QueryFloatAttribute(attributeY, &value.y));
return result;
}
XMLError query_vec3(XMLElement* element, const char* attributeX, const char* attributeY, const char* attributeZ,
glm::vec3& value)
{
auto result = element->QueryFloatAttribute(attributeX, &value.x);
result = query_result_merge(result, element->QueryFloatAttribute(attributeY, &value.y));
result = query_result_merge(result, element->QueryFloatAttribute(attributeZ, &value.z));
return result;
}
XMLError set_bool_attribute(XMLElement* element, const char* attribute, bool value)
{
element->SetAttribute(attribute, value ? "true" : "false");
return XML_SUCCESS;
}
XMLError set_ivec2_attribute(XMLElement* element, const char* attributeX, const char* attributeY,
const glm::ivec2& value)
{
element->SetAttribute(attributeX, value.x);
element->SetAttribute(attributeY, value.y);
return XML_SUCCESS;
}
XMLError set_vec2_attribute(XMLElement* element, const char* attributeX, const char* attributeY,
const glm::vec2& value)
{
element->SetAttribute(attributeX, value.x);
element->SetAttribute(attributeY, value.y);
return XML_SUCCESS;
}
XMLError set_vec3_attribute(XMLElement* element, const char* attributeX, const char* attributeY,
const char* attributeZ, const glm::vec3& value)
{
element->SetAttribute(attributeX, value.x);
element->SetAttribute(attributeY, value.y);
element->SetAttribute(attributeZ, value.z);
return XML_SUCCESS;
}
XMLError query_float_optional_attribute(XMLElement* element, const char* attribute, std::optional<float>& value)
{
value.emplace();
auto result = element->QueryFloatAttribute(attribute, &*value);
if (result == XML_NO_ATTRIBUTE) value.reset();
return result;
}
XMLError query_int_optional_attribute(XMLElement* element, const char* attribute, std::optional<int>& value)
{
value.emplace();
auto result = element->QueryIntAttribute(attribute, &*value);
if (result == XML_NO_ATTRIBUTE) value.reset();
return result;
}
XMLError document_load(const physfs::Path& path, XMLDocument& document)
{
if (!path.is_valid())
{
logger.error(std::format("Failed to open XML document: {} ({})", path.c_str(), physfs::error_get()));
return XML_ERROR_FILE_NOT_FOUND;
}
auto buffer = path.read();
if (buffer.empty())
{
logger.error(std::format("Failed to read XML document: {} ({})", path.c_str(), physfs::error_get()));
return XML_ERROR_FILE_COULD_NOT_BE_OPENED;
}
auto result = document.Parse((const char*)buffer.data(), buffer.size());
if (result != XML_SUCCESS)
logger.error(std::format("Failed to parse XML document: {} ({})", path.c_str(), document.ErrorStr()));
return result;
}
XMLError query_event_id(XMLElement* element, const char* name, const Anm2& anm2, int& eventID)
{
std::string string{};
auto result = query_string_attribute(element, name, &string);
if (result != XML_SUCCESS) return result;
if (anm2.eventMap.contains(string))
{
eventID = anm2.eventMap.at(string);
return XML_SUCCESS;
}
else
{
logger.error(std::format("Could not query anm2 event ID: {} ({})", string, anm2.path));
eventID = -1;
return XML_ERROR_PARSING_ATTRIBUTE;
}
}
XMLError query_layer_id(XMLElement* element, const char* name, const Anm2& anm2, int& layerID)
{
std::string string{};
auto result = query_string_attribute(element, name, &string);
if (result != XML_SUCCESS) return result;
if (anm2.layerMap.contains(string))
{
layerID = anm2.layerMap.at(string);
return XML_SUCCESS;
}
else
{
logger.error(std::format("Could not query anm2 layer ID: {} ({})", string, anm2.path));
layerID = -1;
return XML_ERROR_PARSING_ATTRIBUTE;
}
}
XMLError query_null_id(XMLElement* element, const char* name, const Anm2& anm2, int& nullID)
{
std::string string{};
auto result = query_string_attribute(element, name, &string);
if (result != XML_SUCCESS) return result;
if (anm2.nullMap.contains(string))
{
nullID = anm2.nullMap.at(string);
return XML_SUCCESS;
}
else
{
logger.error(std::format("Could not query anm2 null ID: {} ({})", string, anm2.path));
nullID = -1;
return XML_ERROR_PARSING_ATTRIBUTE;
}
}
XMLError query_anm2(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Anm2& anm2, Anm2::Flags flags)
{
std::string string{};
auto result = query_string_attribute(element, name, &string);
if (result != XML_SUCCESS) return result;
anm2 = Anm2(physfs::Path(archive + "/" + rootPath + "/" + string), flags);
return XML_SUCCESS;
}
XMLError query_texture(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Texture& texture)
{
std::string string{};
auto result = query_string_attribute(element, name, &string);
if (result != XML_SUCCESS) return result;
texture = Texture(physfs::Path(archive + "/" + rootPath + "/" + string));
return XML_SUCCESS;
}
XMLError query_sound(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Audio& sound)
{
std::string string{};
auto result = query_string_attribute(element, name, &string);
if (result != XML_SUCCESS) return result;
sound = Audio(physfs::Path(archive + "/" + rootPath + "/" + string));
return XML_SUCCESS;
}
XMLError query_font(XMLElement* element, const char* name, const std::string& archive, const std::string& rootPath,
Font& font)
{
std::string string{};
auto result = query_string_attribute(element, name, &string);
if (result != XML_SUCCESS) return result;
font = Font(physfs::Path(archive + "/" + rootPath + "/" + string));
return XML_SUCCESS;
}
XMLError query_animation_entry(XMLElement* element, AnimationEntry& animationEntry)
{
auto result = query_string_attribute(element, "Animation", &animationEntry.animation);
result = query_result_merge(result, element->QueryFloatAttribute("Weight", &animationEntry.weight));
return result;
}
XMLError query_animation_entry_collection(XMLElement* element, const char* name,
AnimationEntryCollection& animationEntryCollection)
{
auto result = XML_SUCCESS;
for (auto child = element->FirstChildElement(name); child; child = child->NextSiblingElement(name))
result = query_result_merge(result, query_animation_entry(child, animationEntryCollection.emplace_back()));
return result;
}
XMLError query_sound_entry(XMLElement* element, const std::string& archive, const std::string& rootPath,
SoundEntry& soundEntry, const std::string& attributeName)
{
auto result = query_sound(element, attributeName.c_str(), archive, rootPath, soundEntry.sound);
result = query_result_merge(result, element->QueryFloatAttribute("Weight", &soundEntry.weight));
return result;
}
XMLError query_sound_entry_collection(XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, SoundEntryCollection& soundEntryCollection,
const std::string& attributeName)
{
auto result = XML_SUCCESS;
for (auto child = element->FirstChildElement(name); child; child = child->NextSiblingElement(name))
result = query_result_merge(
result, query_sound_entry(child, archive, rootPath, soundEntryCollection.emplace_back(), attributeName));
return result;
}
}

62
src/resource/xml/util.hpp Normal file
View File

@@ -0,0 +1,62 @@
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <glm/glm.hpp>
#include <tinyxml2.h>
#include "animation_entry.hpp"
#include "sound_entry.hpp"
#include "../../util/physfs.hpp"
#include "../font.hpp"
#include "anm2.hpp"
namespace game::resource::xml
{
tinyxml2::XMLError query_string_attribute(tinyxml2::XMLElement*, const char*, std::string*);
tinyxml2::XMLError query_bool_attribute(tinyxml2::XMLElement*, const char*, bool*);
tinyxml2::XMLError query_path_attribute(tinyxml2::XMLElement*, const char*, std::filesystem::path*);
tinyxml2::XMLError query_color_attribute(tinyxml2::XMLElement*, const char*, float*);
tinyxml2::XMLError query_ivec2(tinyxml2::XMLElement*, const char*, const char*, glm::ivec2&);
tinyxml2::XMLError query_vec2(tinyxml2::XMLElement*, const char*, const char*, glm::vec2&);
tinyxml2::XMLError query_vec3(tinyxml2::XMLElement*, const char*, const char*, const char*, glm::vec3&);
tinyxml2::XMLError set_bool_attribute(tinyxml2::XMLElement*, const char*, bool);
tinyxml2::XMLError set_ivec2_attribute(tinyxml2::XMLElement*, const char*, const char*, const glm::ivec2&);
tinyxml2::XMLError set_vec2_attribute(tinyxml2::XMLElement*, const char*, const char*, const glm::vec2&);
tinyxml2::XMLError set_vec3_attribute(tinyxml2::XMLElement*, const char*, const char*, const char*,
const glm::vec3&);
tinyxml2::XMLError query_float_optional_attribute(tinyxml2::XMLElement* element, const char* attribute,
std::optional<float>& value);
tinyxml2::XMLError query_int_optional_attribute(tinyxml2::XMLElement* element, const char* attribute,
std::optional<int>& 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);
tinyxml2::XMLError query_null_id(tinyxml2::XMLElement* element, const char* name, const Anm2& anm2, int& nullID);
tinyxml2::XMLError query_anm2(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Anm2& anm2, Anm2::Flags flags = {});
tinyxml2::XMLError query_texture(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Texture& texture);
tinyxml2::XMLError query_sound(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Audio& sound);
tinyxml2::XMLError query_font(tinyxml2::XMLElement* element, const char* name, const std::string& archive,
const std::string& rootPath, Font& font);
tinyxml2::XMLError query_animation_entry(tinyxml2::XMLElement* element, AnimationEntry& animationEntry);
tinyxml2::XMLError query_animation_entry_collection(tinyxml2::XMLElement* element, const char* name,
AnimationEntryCollection& animationEntryCollection);
tinyxml2::XMLError query_sound_entry(tinyxml2::XMLElement* element, const std::string& archive,
const std::string& rootPath, SoundEntry& soundEntry,
const std::string& attributeName = "Sound");
tinyxml2::XMLError query_sound_entry_collection(tinyxml2::XMLElement* element, const char* name,
const std::string& archive, const std::string& rootPath,
SoundEntryCollection& soundEntryCollection,
const std::string& attributeName = "Sound");
tinyxml2::XMLError document_load(const util::physfs::Path&, tinyxml2::XMLDocument&);
}

View File

@@ -1,7 +1,10 @@
#include "resources.h"
#include "resources.hpp"
#include "util/preferences.hpp"
#include "log.hpp"
using namespace game::resource;
using namespace game::anm2;
using namespace game::util;
namespace game
{
@@ -10,15 +13,42 @@ namespace game
for (int i = 0; i < shader::COUNT; i++)
shaders[i] = Shader(shader::INFO[i].vertex, shader::INFO[i].fragment);
for (int i = 0; i < audio::COUNT; i++)
audio[i] = Audio(audio::PATHS[i]);
std::string CHARACTERS_DIRECTORY{"resources/characters"};
std::error_code ec{};
for (int i = 0; i < texture::COUNT; i++)
textures[i] = Texture(texture::PATHS[i]);
if (!std::filesystem::is_directory(CHARACTERS_DIRECTORY, ec))
{
if (ec)
logger.warning("Failed to read characters directory: " + CHARACTERS_DIRECTORY + " (" + ec.message() + ")");
else
logger.warning("Characters directory not found: " + CHARACTERS_DIRECTORY);
return;
}
for (int i = 0; i < anm2::COUNT; i++)
anm2s[i] = Anm2(anm2::PATHS[i]);
for (auto& entry : std::filesystem::recursive_directory_iterator(CHARACTERS_DIRECTORY))
if (entry.is_regular_file() && entry.path().extension() == ".zip") characterPreviews.emplace_back(entry.path());
characters.resize(characterPreviews.size());
}
void Resources::sound_play(audio::Type type) { audio[type].play(); }
void Resources::volume_set(float vol) { Audio::volume_set(vol); }
resource::xml::Character& Resources::character_get(int index)
{
if (!characters.at(index).has_value())
{
characters[index].emplace(characterPreviews.at(index).path);
characters[index]->save = characterPreviews.at(index).save;
}
return *characters[index];
}
resource::xml::CharacterPreview& Resources::character_preview_get(int index) { return characterPreviews.at(index); }
void Resources::character_save_set(int index, const resource::xml::Save& save)
{
characterPreviews.at(index).save = save;
if (characters.at(index).has_value()) characters[index]->save = save;
}
Resources::~Resources() { settings.serialize(preferences::path() / "settings.xml"); }
}

View File

@@ -1,128 +0,0 @@
#pragma once
#include "resource/anm2.h"
#include "resource/audio.h"
#include "resource/dialogue.h"
#include "resource/font.h"
#include "resource/shader.h"
namespace game::audio
{
#define AUDIO \
X(BLIP, "resources/sfx/blip.ogg") \
X(ADVANCE, "resources/sfx/advance.ogg") \
X(BOUNCE, "resources/sfx/bounce.ogg") \
X(BURP_1, "resources/sfx/burp1.ogg") \
X(BURP_2, "resources/sfx/burp2.ogg") \
X(BURP_3, "resources/sfx/burp3.ogg") \
X(GRAB, "resources/sfx/grab.ogg") \
X(MISS, "resources/sfx/miss.ogg") \
X(OK, "resources/sfx/ok.ogg") \
X(GOOD, "resources/sfx/good.ogg") \
X(GREAT, "resources/sfx/great.ogg") \
X(EXCELLENT, "resources/sfx/excellent.ogg") \
X(PERFECT, "resources/sfx/perfect.ogg") \
X(FALL, "resources/sfx/fall.ogg") \
X(COMMON, "resources/sfx/common.ogg") \
X(UNCOMMON, "resources/sfx/uncommon.ogg") \
X(RARE, "resources/sfx/rare.ogg") \
X(EPIC, "resources/sfx/epic.ogg") \
X(LEGENDARY, "resources/sfx/legendary.ogg") \
X(SPECIAL, "resources/sfx/special.ogg") \
X(IMPOSSIBLE, "resources/sfx/impossible.ogg") \
X(SCORE_LOSS, "resources/sfx/scoreLoss.ogg") \
X(HIGH_SCORE, "resources/sfx/highScore.ogg") \
X(HIGH_SCORE_BIG, "resources/sfx/highScoreBig.ogg") \
X(HIGH_SCORE_LOSS, "resources/sfx/highScoreLoss.ogg") \
X(GURGLE_1, "resources/sfx/gurgle1.ogg") \
X(GURGLE_2, "resources/sfx/gurgle2.ogg") \
X(GURGLE_3, "resources/sfx/gurgle3.ogg") \
X(RELEASE, "resources/sfx/release.ogg") \
X(SUMMON, "resources/sfx/summon.ogg") \
X(RETURN, "resources/sfx/return.ogg") \
X(DISPOSE, "resources/sfx/dispose.ogg") \
X(RUB, "resources/sfx/rub.ogg") \
X(THROW, "resources/sfx/throw.ogg")
enum Type
{
#define X(symbol, path) symbol,
AUDIO
#undef X
COUNT
};
static constexpr const char* PATHS[] = {
#define X(symbol, path) path,
AUDIO
#undef X
};
#undef AUDIO
static constexpr Type BURPS[] = {BURP_1, BURP_2, BURP_3};
static constexpr Type GURGLES[] = {GURGLE_1, GURGLE_2, GURGLE_3};
}
namespace game::texture
{
#define TEXTURES X(BG, "resources/gfx/bg.png")
enum Type
{
#define X(symbol, path) symbol,
TEXTURES
#undef X
COUNT
};
static constexpr const char* PATHS[] = {
#define X(symbol, path) path,
TEXTURES
#undef X
};
#undef TEXTURES
}
namespace game::anm2
{
#define ANM2 \
X(CHARACTER, "resources/anm2/snivy.anm2") \
X(ITEMS, "resources/anm2/items.anm2") \
X(CURSOR, "resources/anm2/cursor.anm2")
enum Anm2Type
{
#define X(symbol, path) symbol,
ANM2
#undef X
COUNT
};
static constexpr const char* PATHS[] = {
#define X(symbol, path) path,
ANM2
#undef X
};
#undef ANM2
}
namespace game
{
class Resources
{
public:
resource::Shader shaders[resource::shader::COUNT];
resource::Audio audio[audio::COUNT];
resource::Texture textures[texture::COUNT];
anm2::Anm2 anm2s[anm2::COUNT];
resource::Font font{"resources/font/font.ttf"};
resource::Dialogue dialogue{"resources/dialogue.xml"};
Resources();
void sound_play(audio::Type);
};
}

32
src/resources.hpp Normal file
View File

@@ -0,0 +1,32 @@
#pragma once
#include <optional>
#include "resource/font.hpp"
#include "resource/shader.hpp"
#include "resource/xml/character.hpp"
#include "resource/xml/character_preview.hpp"
#include "resource/xml/settings.hpp"
namespace game
{
class Resources
{
public:
resource::Shader shaders[resource::shader::COUNT];
resource::Font font{"resources/font/font.ttf"};
resource::xml::Settings settings;
std::vector<resource::xml::CharacterPreview> characterPreviews{};
std::vector<std::optional<resource::xml::Character>> characters{};
Resources();
~Resources();
resource::xml::Character& character_get(int index);
resource::xml::CharacterPreview& character_preview_get(int index);
void character_save_set(int index, const resource::xml::Save& save);
void volume_set(float volume);
};
}

View File

@@ -1,14 +1,14 @@
#include "state.h"
#include "state.hpp"
#include <backends/imgui_impl_opengl3.h>
#include <backends/imgui_impl_sdl3.h>
#include <imgui.h>
#include "util/math_.h"
#include "util/math.hpp"
using namespace glm;
using namespace game::resource;
using namespace game::util;
using namespace game::state;
namespace game
{
@@ -17,403 +17,117 @@ namespace game
constexpr auto UPDATE_RATE = 60;
constexpr auto UPDATE_INTERVAL = (1000 / UPDATE_RATE);
State::State(SDL_Window* inWindow, SDL_GLContext inContext, vec2 size)
: window(inWindow), context(inContext), canvas(size, true)
State::State(SDL_Window* _window, SDL_GLContext _context, resource::xml::Settings settings)
: window(_window), context(_context), canvas(settings.windowSize, Canvas::DEFAULT)
{
resources.settings = settings;
SDL_SetWindowSize(window, resources.settings.windowSize.x, resources.settings.windowSize.y);
}
void State::tick()
{
for (auto& item : items)
item.tick();
character.tick();
if (character.isJustDigestionStart)
resources.sound_play(audio::GURGLES[(int)math::random_roll(std::size(audio::GURGLES))]);
if (character.is_event("Burp")) resources.sound_play(audio::BURPS[(int)math::random_roll(std::size(audio::BURPS))]);
if (character.isJustDigestionEnd && !character.isJustStageUp)
switch (type)
{
character.state_set(Character::PAT, true);
textWindow.set_random(resources.dialogue.postDigestIDs, resources, character);
case SELECT:
select.tick();
break;
case PLAY:
play.tick(resources);
break;
default:
break;
}
cursor.tick();
textWindow.tick(resources, character);
}
void State::update()
{
static bool isRubbing{};
auto& inventory = mainMenuWindow.inventory;
auto& dialogue = resources.dialogue;
int width{};
int height{};
SDL_GetWindowSize(window, &width, &height);
#ifndef __EMSCRIPTEN__
SDL_GetWindowSize(window, &resources.settings.windowSize.x, &resources.settings.windowSize.y);
#endif
SDL_Event event;
while (SDL_PollEvent(&event))
{
ImGui_ImplSDL3_ProcessEvent(&event);
if (event.type == SDL_EVENT_QUIT) isRunning = false;
if (event.type == SDL_EVENT_QUIT)
{
if (type == PLAY) play.exit(resources);
isRunning = false;
}
if (!isRunning) return;
}
if (!isRunning) return;
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
auto style = ImGui::GetStyle();
if (textWindow.isFlagActivated)
switch (type)
{
switch (textWindow.flag)
{
case Dialogue::Entry::ACTIVATE_WINDOWS:
isInfo = true;
isMainMenu = true;
break;
case Dialogue::Entry::DEACTIVATE_WINDOWS:
isInfo = false;
isMainMenu = false;
break;
case Dialogue::Entry::ONLY_INFO:
isInfo = true;
isMainMenu = false;
break;
case Dialogue::Entry::ACTIVATE_CHEATS:
mainMenuWindow.isCheats = true;
break;
default:
break;
}
case SELECT:
select.update(resources);
if (select.info.isNewGame || select.info.isContinue)
{
Play::Game game = select.info.isNewGame ? Play::NEW_GAME : Play::CONTINUE;
if (game == Play::NEW_GAME) resources.character_save_set(select.characterIndex, resource::xml::Save());
play.set(resources, select.characterIndex, game);
type = PLAY;
select.info.isNewGame = false;
select.info.isContinue = false;
}
break;
case PLAY:
play.update(resources);
if (play.menu.settingsMenu.isGoToSelect)
{
play.exit(resources);
type = SELECT;
play.menu.settingsMenu.isGoToSelect = false;
}
break;
default:
break;
}
auto infoSize = ImVec2(width * 0.5f, ImGui::GetTextLineHeightWithSpacing() * 3.5);
auto infoPos = style.WindowPadding;
if (isInfo) infoWindow.update(resources, gameData, character, infoSize, infoPos);
auto textSize = ImVec2(width - style.WindowPadding.x * 2, ImGui::GetTextLineHeightWithSpacing() * 8);
auto textPos = ImVec2(style.WindowPadding.x, height - textSize.y - style.WindowPadding.y);
if (isText) textWindow.update(resources, character, textSize, textPos);
auto mainSize =
ImVec2((width * 0.5f) - style.WindowPadding.x * 3, height - textSize.y - (style.WindowPadding.y * 3));
auto mainPos = ImVec2(infoPos.x + infoSize.x + style.WindowPadding.x, style.WindowPadding.y);
if (isMainMenu) mainMenuWindow.update(resources, character, gameData, textWindow, mainSize, mainPos);
/* Inventory */
if (inventory.isQueued)
auto isHideCursor = type == PLAY;
if (isHideCursor != isCursorHidden)
{
if (items.size() > Item::DEPLOYED_MAX)
{
inventory.isQueued = false;
inventory.queuedItemType = Item::NONE;
resources.sound_play(audio::MISS);
}
if (isHideCursor)
SDL_HideCursor();
else
{
auto& type = inventory.queuedItemType;
auto position = glm::vec2(((Item::SPAWN_X_MAX - Item::SPAWN_X_MIN) * math::random()) + Item::SPAWN_X_MIN,
((Item::SPAWN_Y_MAX - Item::SPAWN_Y_MIN) * math::random()) + Item::SPAWN_Y_MIN);
items.emplace_back(&resources.anm2s[anm2::ITEMS], position, type);
inventory.values[type]--;
if (inventory.values[type] == 0) inventory.values.erase(type);
type = Item::NONE;
inventory.isQueued = false;
}
SDL_ShowCursor();
isCursorHidden = isHideCursor;
}
/* Item */
Item::hoveredItem = nullptr;
Item::heldItemPrevious = Item::heldItem;
bool isSpeak{};
std::vector<int>* dialogueIDs{};
for (int i = 0; i < items.size(); i++)
{
auto& item = items[i];
item.update(resources);
if (&item == Item::heldItem && &item != &items.back())
{
std::swap(items[i], items.back());
Item::heldItem = &items.back();
continue;
}
if (item.isToBeDeleted)
{
items.erase(items.begin() + i--);
continue;
}
if (Item::queuedReturnItem == &item)
{
if (Item::queuedReturnItem->state == Item::DEFAULT)
{
inventory.values[item.type]++;
resources.sound_play(audio::RETURN);
}
else
resources.sound_play(audio::DISPOSE);
items.erase(items.begin() + i--);
Item::queuedReturnItem = nullptr;
continue;
}
}
auto& item = Item::heldItem;
auto isHeldItemChanged = item != Item::heldItemPrevious;
if (item && character.state != Character::STAGE_UP)
{
auto& type = item->type;
auto& calories = Item::CALORIES[type];
auto& category = Item::CATEGORIES[type];
auto& position = item->position;
auto caloriesChew = Item::CALORIES[type] / (Item::CHEW_COUNT_MAX + 1);
auto digestionRateBonusChew = Item::DIGESTION_RATE_BONUSES[type] / (Item::CHEW_COUNT_MAX + 1);
auto eatSpeedBonusChew = Item::EAT_SPEED_BONUSES[type] / (Item::CHEW_COUNT_MAX + 1);
auto isAbleToEat = character.calories + caloriesChew <= character.max_capacity();
if (category == Item::FOOD)
{
auto isByMouth = math::is_point_in_rectf(character.mouth_rect_get(), position);
if (character.state == Character::EAT && !isHeldItemChanged)
{
if (!isByMouth)
{
if (character.is_over_capacity())
{
dialogueIDs = &dialogue.foodEasedIDs;
character.state_set(Character::IDLE);
}
else
{
dialogueIDs = &dialogue.foodStolenIDs;
character.state_set(Character::ANGRY);
}
isSpeak = true;
}
if (character.is_event(Character::EVENT_EAT))
{
item->chewCount++;
if (digestionRateBonusChew > 0 || digestionRateBonusChew < 0)
{
character.digestionRate =
glm::clamp(Character::DIGESTION_RATE_MIN, character.digestionRate + digestionRateBonusChew,
Character::DIGESTION_RATE_MAX);
}
if (eatSpeedBonusChew > 0)
{
character.eatSpeedMultiplier =
glm::clamp(Character::EAT_SPEED_MULTIPLIER_MIN, character.eatSpeedMultiplier + eatSpeedBonusChew,
Character::EAT_SPEED_MULTIPLIER_MAX);
}
character.calories += caloriesChew;
character.totalCaloriesConsumed += caloriesChew;
character.consume_played_event();
if (item->chewCount > Item::CHEW_COUNT_MAX)
{
character.isFinishedFood = true;
character.foodItemsEaten++;
item->isToBeDeleted = true;
Item::heldItem = nullptr;
cursor.play(Cursor::ANIMATION_DEFAULT);
}
else
item->state_set(item->chewCount == 1 ? Item::CHEW_1
: item->chewCount == 2 ? Item::CHEW_2
: Item::DEFAULT);
if (character.calories + caloriesChew >= character.max_capacity() && Item::heldItem)
{
character.state_set(Character::SHOCKED);
dialogueIDs = &dialogue.fullIDs;
isSpeak = true;
}
}
}
else if (character.state == Character::ANGRY)
{
}
else
{
cursor.play(Cursor::ANIMATION_GRAB);
if (!isAbleToEat)
{
character.state_set(Character::SHOCKED);
if (caloriesChew > character.max_capacity())
dialogueIDs = &dialogue.capacityLowIDs;
else
dialogueIDs = &dialogue.fullIDs;
}
else if (character.is_over_capacity())
dialogueIDs = &dialogue.feedFullIDs;
else
{
character.state_set(Character::EAGER);
dialogueIDs = &dialogue.feedHungryIDs;
}
if (isHeldItemChanged) isSpeak = true;
if (isAbleToEat && isByMouth)
if (character.state != Character::EAT) character.state_set(Character::EAT);
isRubbing = false;
}
}
}
else if (isHeldItemChanged && character.state != Character::ANGRY && character.state != Character::STAGE_UP)
character.state_set(Character::IDLE);
/* Character */
if (character.isFinishedFood && character.state == Character::IDLE)
{
if (!character.is_over_capacity()) dialogueIDs = &dialogue.eatHungryIDs;
if (math::random_percent_roll(Character::BURP_BIG_CHANCE))
{
character.state_set(Character::BURP_BIG);
dialogueIDs = &dialogue.burpBigIDs;
}
else if (math::random_percent_roll(Character::BURP_SMALL_CHANCE))
{
character.state_set(Character::BURP_SMALL);
dialogueIDs = &dialogue.burpSmallIDs;
}
else if (!character.is_over_capacity() && math::random_percent_roll(Character::PAT_CHANCE))
character.state_set(Character::PAT);
character.isFinishedFood = false;
isSpeak = true;
}
/* Character */
if (character.isJustAppeared)
{
textWindow.set(dialogue.get("Start"), character);
isText = true;
character.isJustAppeared = false;
}
if (character.isJustFinalThreshold && !character.isFinalThresholdReached)
{
Item::heldItem = nullptr;
Item::heldItemPrevious = nullptr;
textWindow.set(dialogue.get("End"), character);
items.clear();
character.isFinalThresholdReached = true;
}
/* Dialogue */
if (isSpeak && dialogueIDs) textWindow.set_random(*dialogueIDs, resources, character);
/* Rubbing/Grabbing */
bool isHeadRubPossible = math::is_point_in_rectf(character.head_rect_get(), cursor.position) && !Item::heldItem;
bool isBellyRubPossible = math::is_point_in_rectf(character.belly_rect_get(), cursor.position) && !Item::heldItem;
bool isTailRubPossible = math::is_point_in_rectf(character.tail_rect_get(), cursor.position) && !Item::heldItem;
auto isRubPossible = isHeadRubPossible || isBellyRubPossible || isTailRubPossible;
if (isRubPossible)
{
if (!isRubbing) cursor.play(Cursor::ANIMATION_HOVER);
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
{
isRubbing = true;
resources.sound_play(audio::RUB);
if (isHeadRubPossible)
{
character.state_set(Character::HEAD_RUB);
cursor.play(Cursor::ANIMATION_RUB);
}
else if (isBellyRubPossible)
{
character.state_set(Character::BELLY_RUB);
cursor.play(Cursor::ANIMATION_GRAB);
}
else if (isTailRubPossible)
{
character.state_set(Character::TAIL_RUB);
cursor.play(Cursor::ANIMATION_GRAB);
}
}
}
else if (Item::hoveredItem)
cursor.play(Cursor::ANIMATION_HOVER);
else if (!Item::heldItem)
cursor.play(Cursor::ANIMATION_DEFAULT);
if (isRubbing)
{
if (isBellyRubPossible && !character.isDigesting)
{
auto delta = ImGui::GetIO().MouseDelta;
auto power = fabs(delta.x) + fabs(delta.y);
if (character.calories > 0) character.digestionProgress += power * Character::DIGESTION_RUB_BONUS;
}
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left) || !isRubPossible)
{
isRubbing = false;
character.state_set(Character::IDLE);
}
}
SDL_HideCursor();
cursor.update();
}
void State::render()
{
SDL_GL_MakeCurrent(window, context);
int width{};
int height{};
SDL_GetWindowSize(window, &width, &height);
auto& textureShader = resources.shaders[shader::TEXTURE];
auto& rectShader = resources.shaders[shader::RECT];
auto& color =
resources.settings.isUseCharacterColor && type == PLAY ? play.character.data.color : resources.settings.color;
auto windowSize = resources.settings.windowSize;
#ifndef __EMSCRIPTEN__
SDL_GetWindowSize(window, &windowSize.x, &windowSize.y);
#endif
canvas.bind();
canvas.clear(glm::vec4(0, 0, 0, 1));
auto bgModel = math::quad_model_get(vec2(width, height));
canvas.texture_render(textureShader, resources.textures[texture::BG].id, bgModel);
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
character.render(textureShader, rectShader, canvas);
for (auto& item : items)
item.render(textureShader, rectShader, canvas);
cursor.render(textureShader, rectShader, canvas);
canvas.size_set(windowSize);
canvas.clear(vec4(color, 1.0f));
canvas.unbind();
switch (type)
{
case SELECT:
select.render(resources, canvas);
break;
case PLAY:
play.render(resources, canvas);
break;
default:
break;
}
SDL_GL_SwapWindow(window);
}

View File

@@ -1,54 +0,0 @@
#pragma once
#include <SDL3/SDL.h>
#include "character.h"
#include "cursor.h"
#include "item.h"
#include "canvas.h"
#include "resources.h"
#include "window/info.h"
#include "window/main_menu.h"
#include "window/text.h"
namespace game
{
class State
{
SDL_Window* window{};
SDL_GLContext context{};
long previousUpdate{};
long previousTick{};
Resources resources;
Character character{&resources.anm2s[anm2::CHARACTER], glm::vec2(300, 500)};
Cursor cursor{&resources.anm2s[anm2::CURSOR]};
std::vector<Item> items{};
window::Info infoWindow;
window::MainMenu mainMenuWindow;
window::Text textWindow;
bool isMainMenu{false};
bool isInfo{false};
bool isText{false};
GameData gameData;
void tick();
void update();
void render();
public:
bool isRunning{true};
Canvas canvas{};
State(SDL_Window*, SDL_GLContext, glm::vec2);
void loop();
};
};

49
src/state.hpp Normal file
View File

@@ -0,0 +1,49 @@
#pragma once
#include <SDL3/SDL.h>
#include "render/canvas.hpp"
#include "resources.hpp"
#include "state/play.hpp"
#include "state/select.hpp"
#include "entity/cursor.hpp"
namespace game
{
class State
{
public:
SDL_Window* window{};
SDL_GLContext context{};
Uint64 previousUpdate{};
Uint64 previousTick{};
enum Type
{
PLAY,
SELECT
};
Type type{SELECT};
Resources resources;
state::Play play;
state::Select select;
void tick();
void tick_60();
void update();
void render();
bool isRunning{true};
bool isCursorHidden{};
Canvas canvas{};
State(SDL_Window*, SDL_GLContext, resource::xml::Settings);
void loop();
};
};

365
src/state/play.cpp Normal file
View File

@@ -0,0 +1,365 @@
#include "play.hpp"
#include <array>
#include <glm/glm.hpp>
#include <imgui.h>
#include <imgui_impl_opengl3.h>
#include "../util/imgui.hpp"
#include "../util/imgui/style.hpp"
#include "../util/imgui/widget.hpp"
#include "../util/math.hpp"
using namespace game::resource;
using namespace game::util;
using namespace game::state::play;
using namespace glm;
namespace game::state
{
World::Focus Play::focus_get()
{
if (!isWindows) return World::CENTER;
return menu.isOpen && tools.isOpen ? World::MENU_TOOLS
: menu.isOpen ? World::MENU
: tools.isOpen ? World::TOOLS
: World::CENTER;
}
void Play::set(Resources& resources, int selectedCharacterIndex, enum Game game)
{
auto& data = resources.character_get(selectedCharacterIndex);
auto& saveData = data.save;
auto& itemSchema = data.itemSchema;
auto& dialogue = data.dialogue;
auto& menuSchema = data.menuSchema;
this->characterIndex = selectedCharacterIndex;
cheatCodeIndex = 0;
cheatCodeStartTime = 0.0;
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);
auto isAlternateSpritesheet =
(game == NEW_GAME && math::random_percent_roll(data.alternateSpritesheet.chanceOnNewGame));
if (isAlternateSpritesheet || saveData.isAlternateSpritesheet)
{
character.spritesheet_set(entity::Character::ALTERNATE);
if (game == NEW_GAME) character.data.alternateSpritesheet.sound.play();
}
character.totalCaloriesConsumed = saveData.totalCaloriesConsumed;
character.totalFoodItemsEaten = saveData.totalFoodItemsEaten;
characterManager = CharacterManager{};
cursor = entity::Cursor(character.data.cursorSchema.anm2);
cursor.interactTypeID = character.data.interactTypeNames.empty() ? -1 : 0;
menu.inventory = Inventory{};
for (auto& [id, quantity] : saveData.inventory)
{
if (quantity == 0) continue;
menu.inventory.values[id] = quantity;
}
itemManager = ItemManager{};
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& saveItem = itemSchema.anm2s.at(item.id);
itemManager.items.emplace_back(saveItem, item.position, item.id, item.chewCount, 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);
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();
text.entry = nullptr;
text.isEnabled = false;
menu.isCheats = false;
isPostgame = saveData.isPostgame;
if (character.stage_get() >= character.stage_max_get()) isPostgame = true;
if (isPostgame) menu.isCheats = true;
if (game == NEW_GAME) isWindows = false;
if (auto font = character.data.menuSchema.font.get()) ImGui::GetIO().FontDefault = font;
character.queue_idle_animation();
character.tick();
worldCanvas.size_set(imgui::to_vec2(ImGui::GetMainViewport()->Size));
world.set(character, worldCanvas, focus_get());
if (game == NEW_GAME && dialogue.start.is_valid())
{
character.queue_play({.animation = dialogue.start.animation, .isInterruptible = false});
character.tick();
isStart = true;
isStartBegin = false;
isStartEnd = false;
}
if (isPostgame)
{
isEnd = true;
isEndBegin = true;
isEndEnd = true;
}
else
{
isEnd = false;
isEndBegin = false;
isEndEnd = false;
}
}
void Play::exit(Resources& resources)
{
imgui::style::color_set(resources.settings.color);
imgui::style::rounding_set();
imgui::widget::sounds_set(nullptr, nullptr);
ImGui::GetIO().FontDefault = resources.font.get();
save(resources);
}
void Play::tick(Resources&)
{
character.tick();
cursor.tick();
menu.tick();
toasts.tick();
text.tick(character);
for (auto& item : itemManager.items)
item.tick();
}
void Play::update(Resources& resources)
{
static constexpr std::array<ImGuiKey, 10> CHEAT_CODE = {
ImGuiKey_UpArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_DownArrow, ImGuiKey_LeftArrow,
ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_B, ImGuiKey_A};
static constexpr std::array<ImGuiKey, 6> CHEAT_INPUT_KEYS = {
ImGuiKey_UpArrow, ImGuiKey_DownArrow, ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_B, ImGuiKey_A};
static constexpr auto CHEAT_CODE_INPUT_TIME_SECONDS = 5.0;
auto focus = focus_get();
auto& dialogue = character.data.dialogue;
if (!menu.isCheats)
{
for (auto key : CHEAT_INPUT_KEYS)
{
if (!ImGui::IsKeyPressed(key, false)) continue;
if (key == CHEAT_CODE[cheatCodeIndex])
{
cheatCodeIndex++;
cheatCodeStartTime = ImGui::GetTime();
}
else if (key == CHEAT_CODE[0])
{
cheatCodeIndex = 1;
cheatCodeStartTime = ImGui::GetTime();
}
else
{
cheatCodeIndex = 0;
cheatCodeStartTime = 0.0;
}
if (cheatCodeIndex >= (int)CHEAT_CODE.size())
{
menu.isCheats = true;
cheatCodeIndex = 0;
cheatCodeStartTime = 0.0;
toasts.push("Cheats unlocked!");
character.data.menuSchema.sounds.cheatsActivated.play();
}
}
if (cheatCodeIndex > 0 && (ImGui::GetTime() - cheatCodeStartTime > CHEAT_CODE_INPUT_TIME_SECONDS))
{
cheatCodeIndex = 0;
cheatCodeStartTime = 0.0;
}
}
if (isWindows)
{
menu.update(resources, itemManager, character, cursor, text, worldCanvas);
tools.update(character, cursor, world, focus, worldCanvas);
info.update(resources, character);
toasts.update();
}
if (text.isEnabled) text.update(character);
if (isStart)
{
if (!isStartBegin)
{
if (auto animation = character.animation_get())
{
if (animation->isLoop || character.state == entity::Actor::STOPPED)
{
text.set(dialogue.get(dialogue.start.id), character);
isStartBegin = true;
}
}
}
else if (!isStartEnd)
{
if (text.entry->is_last())
{
isWindows = true;
isStartEnd = true;
isStart = false;
world.character_focus(character, worldCanvas, focus_get());
}
}
}
if (character.isJustStageFinal && !isEnd && !isPostgame) isEnd = true;
if (isEnd)
{
if (!isEndBegin)
{
if (character.is_animation_finished())
{
text.set(dialogue.get(dialogue.end.id), character);
isEndBegin = true;
isWindows = false;
tools.isOpen = false;
menu.isOpen = false;
character.calories = 0;
character.digestionProgress = 0;
itemManager.items.clear();
itemManager.heldItemIndex = -1;
world.character_focus(character, worldCanvas, focus_get());
}
}
else if (!isEndEnd)
{
if (text.entry->is_last())
{
menu.isOpen = true;
isWindows = true;
isEndEnd = true;
isEnd = false;
isPostgame = true;
menu.isCheats = true;
world.character_focus(character, worldCanvas, focus_get());
}
}
}
itemManager.update(character, cursor, areaManager, text, World::BOUNDS, worldCanvas);
characterManager.update(character, cursor, text, worldCanvas);
character.update();
cursor.update();
world.update(character, cursor, worldCanvas, focus);
if (autosaveTime += ImGui::GetIO().DeltaTime; autosaveTime > AUTOSAVE_TIME || menu.settingsMenu.isSave)
{
save(resources);
autosaveTime = 0;
menu.settingsMenu.isSave = false;
}
}
void Play::render(Resources& resources, Canvas& canvas)
{
auto& textureShader = resources.shaders[shader::TEXTURE];
auto& rectShader = resources.shaders[shader::RECT];
auto size = imgui::to_ivec2(ImGui::GetMainViewport()->Size);
auto& bgTexture = character.data.areaSchema.areas.at(areaManager.get(character)).texture;
auto windowModel = math::quad_model_get(vec2(size));
auto worldModel = math::quad_model_get(bgTexture.size);
worldCanvas.bind();
worldCanvas.size_set(size);
worldCanvas.clear();
worldCanvas.texture_render(textureShader, bgTexture.id, worldModel);
character.render(textureShader, rectShader, worldCanvas);
for (auto& item : itemManager.items)
item.render(textureShader, rectShader, worldCanvas);
if (menu.debug.isBoundsDisplay)
{
auto boundsModel =
math::quad_model_get(glm::vec2(World::BOUNDS.z, World::BOUNDS.w), glm::vec2(World::BOUNDS.x, World::BOUNDS.y),
glm::vec2(World::BOUNDS.x, World::BOUNDS.y) * 0.5f);
worldCanvas.rect_render(rectShader, boundsModel);
}
worldCanvas.unbind();
canvas.bind();
canvas.texture_render(textureShader, worldCanvas.texture, windowModel);
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
cursor.render(textureShader, rectShader, canvas);
canvas.unbind();
}
void Play::save(Resources& resources)
{
resource::xml::Save save;
save.weight = character.weight;
save.calories = character.calories;
save.capacity = character.capacity;
save.digestionRate = character.digestionRate;
save.eatSpeed = character.eatSpeed;
save.digestionProgress = character.digestionProgress;
save.isDigesting = character.isDigesting;
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.isPostgame = isPostgame;
save.isAlternateSpritesheet = character.spritesheetType == entity::Character::ALTERNATE;
for (auto& [id, quantity] : menu.inventory.values)
{
if (quantity == 0) continue;
save.inventory[id] = quantity;
}
for (auto& item : itemManager.items)
save.items.emplace_back(item.schemaID, item.chewCount, item.position, item.velocity,
*item.overrides[item.rotationOverrideID].frame.rotation);
save.isValid = true;
resources.character_save_set(characterIndex, save);
save.serialize(character.data.save_path_get());
toasts.push("Saving...");
}
};

71
src/state/play.hpp Normal file
View File

@@ -0,0 +1,71 @@
#pragma once
#include "../resources.hpp"
#include "play/area_manager.hpp"
#include "play/character_manager.hpp"
#include "play/info.hpp"
#include "play/item_manager.hpp"
#include "play/menu.hpp"
#include "play/text.hpp"
#include "play/toasts.hpp"
#include "play/tools.hpp"
#include "play/world.hpp"
namespace game::state
{
class Play
{
public:
static constexpr auto AUTOSAVE_TIME = 30.0f;
enum Game
{
NEW_GAME,
CONTINUE
};
entity::Character character;
entity::Cursor cursor;
play::Info info;
play::Menu menu;
play::Tools tools;
play::Text text;
play::World world;
play::Toasts toasts;
play::ItemManager itemManager{};
play::CharacterManager characterManager{};
play::AreaManager areaManager{};
int characterIndex{};
int areaIndex{};
float autosaveTime{};
int cheatCodeIndex{};
double cheatCodeStartTime{};
bool isWindows{true};
bool isStartBegin{};
bool isStart{};
bool isStartEnd{};
bool isEndBegin{};
bool isEnd{};
bool isEndEnd{};
bool isPostgame{};
Canvas worldCanvas{play::World::SIZE};
Play() = default;
void set(Resources&, int characterIndex, Game = CONTINUE);
void exit(Resources& resources);
void update(Resources&);
void tick(Resources&);
void render(Resources&, Canvas&);
void save(Resources&);
play::World::Focus focus_get();
};
};

View File

@@ -0,0 +1,433 @@
#include "skill_check.hpp"
#include <imgui_internal.h>
#include "../../../util/imgui.hpp"
#include "../../../util/imgui/widget.hpp"
#include "../../../util/math.hpp"
#include <cmath>
#include <format>
#include <ranges>
using namespace game::util;
using namespace game::entity;
using namespace game::resource;
using namespace glm;
namespace game::state::play
{
float SkillCheck::accuracy_score_get(entity::Character& character)
{
if (totalPlays == 0) return 0.0f;
auto& schema = character.data.skillCheckSchema;
float combinedWeight{};
for (int i = 0; i < (int)schema.grades.size(); i++)
{
auto& grade = schema.grades[i];
combinedWeight += gradeCounts[i] * grade.weight;
}
return glm::clamp(0.0f, math::to_percent(combinedWeight / totalPlays), 100.0f);
}
SkillCheck::Challenge SkillCheck::challenge_generate(entity::Character& character)
{
auto& schema = character.data.skillCheckSchema;
Challenge newChallenge;
Range newRange{};
auto rangeSize = std::max(schema.rangeMin, schema.rangeBase - (schema.rangeScoreBonus * score));
newRange.min = math::random_max(1.0f - rangeSize);
newRange.max = newRange.min + rangeSize;
newChallenge.range = newRange;
newChallenge.tryValue = 0.0f;
newChallenge.speed =
glm::clamp(schema.speedMin, schema.speedMin + (schema.speedScoreBonus * score), schema.speedMax);
if (math::random_bool())
{
newChallenge.tryValue = 1.0f;
newChallenge.speed *= -1;
}
return newChallenge;
}
SkillCheck::SkillCheck(entity::Character& character) { challenge = challenge_generate(character); }
void SkillCheck::tick()
{
for (auto& [i, actor] : itemActors)
actor.tick();
}
void 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);
static constexpr ImVec4 PERFECT_COLOR = ImVec4(1, 1, 1, 0.50);
static constexpr auto LINE_HEIGHT = 5.0f;
static constexpr auto LINE_WIDTH_BONUS = 10.0f;
static constexpr auto TOAST_MESSAGE_SPEED = 1.0f;
static constexpr auto ITEM_FALL_GRAVITY = 2400.0f;
auto& dialogue = character.data.dialogue;
auto& schema = character.data.skillCheckSchema;
auto& itemSchema = character.data.itemSchema;
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 cursorPos = ImGui::GetCursorPos();
ImGui::Text("Score: %i pts (%ix)", score, combo);
auto bestString = std::format("Best: {} pts({}x)", highScore, bestCombo);
ImGui::SetCursorPos(ImVec2(size.x - ImGui::CalcTextSize(bestString.c_str()).x, cursorPos.y));
ImGui::Text("Best: %i pts (%ix)", 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!");
}
auto barMin = ImVec2(position.x + (size.x * 0.5f) - (spacing * 0.5f), position.y + (spacing * 2.0f));
auto barMax = ImVec2(barMin.x + (spacing * 2.0f), barMin.y + size.y - (spacing * 4.0f));
auto endTimerProgress = (float)endTimer / endTimerMax;
auto bgColor = ImGui::GetStyleColorVec4(ImGuiCol_FrameBg);
bgColor = imgui::to_imvec4(imgui::to_vec4(bgColor) * BG_COLOR_MULTIPLIER);
drawList->AddRectFilled(barMin, barMax, ImGui::GetColorU32(bgColor));
auto barWidth = barMax.x - barMin.x;
auto barHeight = barMax.y - barMin.y;
auto sub_ranges_get = [&](Range& range)
{
auto& min = range.min;
auto& max = range.max;
std::vector<Range> ranges{};
auto baseHeight = max - min;
auto center = (min + max) * 0.5f;
int rangeCount{};
for (auto& grade : schema.grades)
{
if (grade.isFailure) continue;
auto scale = powf(0.5f, (float)rangeCount);
auto halfHeight = baseHeight * scale * 0.5f;
rangeCount++;
ranges.push_back({center - halfHeight, center + halfHeight});
}
return ranges;
};
auto range_draw = [&](Range& range, float alpha = 1.0f)
{
auto subRanges = sub_ranges_get(range);
for (int i = 0; i < (int)subRanges.size(); i++)
{
auto& subRange = subRanges[i];
int layer = (int)subRanges.size() - 1 - i;
ImVec2 rectMin = {barMin.x, barMin.y + subRange.min * barHeight};
ImVec2 rectMax = {barMax.x, barMin.y + subRange.max * barHeight};
ImVec4 color =
i == (int)subRanges.size() - 1 ? PERFECT_COLOR : ImGui::GetStyleColorVec4(ImGuiCol_FrameBgHovered);
color.w = (color.w - (float)layer / subRanges.size()) * alpha;
drawList->AddRectFilled(rectMin, rectMax, ImGui::GetColorU32(color));
}
};
range_draw(challenge.range, isActive ? 1.0f : 0.0f);
auto lineMin = ImVec2(barMin.x - LINE_WIDTH_BONUS, barMin.y + (barHeight * tryValue));
auto lineMax = ImVec2(barMin.x + barWidth + LINE_WIDTH_BONUS, lineMin.y + LINE_HEIGHT);
auto lineColor = LINE_COLOR;
lineColor.w = isActive ? 1.0f : endTimerProgress;
drawList->AddRectFilled(lineMin, lineMax, ImGui::GetColorU32(lineColor));
if (!isActive && !isGameOver)
{
range_draw(queuedChallenge.range, 1.0f - endTimerProgress);
auto queuedLineMin = ImVec2(barMin.x - LINE_WIDTH_BONUS, barMin.y + (barHeight * queuedChallenge.tryValue));
auto queuedLineMax = ImVec2(barMin.x + barWidth + LINE_WIDTH_BONUS, queuedLineMin.y + LINE_HEIGHT);
auto queuedLineColor = LINE_COLOR;
queuedLineColor.w = 1.0f - endTimerProgress;
drawList->AddRectFilled(queuedLineMin, queuedLineMax, ImGui::GetColorU32(queuedLineColor));
}
if (isActive)
{
tryValue += challenge.speed;
if (tryValue > 1.0f || tryValue < 0.0f)
{
tryValue = tryValue > 1.0f ? 0.0f : tryValue < 0.0f ? 1.0f : tryValue;
if (score > 0)
{
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);
}
}
ImGui::SetCursorScreenPos(barMin);
auto barButtonSize = ImVec2(barMax.x - barMin.x, barMax.y - barMin.y);
if (ImGui::IsKeyPressed(ImGuiKey_Space) ||
WIDGET_FX(ImGui::InvisibleButton("##SkillCheckBar", barButtonSize, ImGuiButtonFlags_PressedOnClick)))
{
int gradeID{};
auto subRanges = sub_ranges_get(challenge.range);
for (int i = 0; i < (int)subRanges.size(); i++)
{
auto& subRange = subRanges[i];
if (tryValue >= subRange.min && tryValue <= subRange.max)
gradeID = std::min((int)gradeID + 1, (int)schema.grades.size() - 1);
}
gradeCounts[gradeID]++;
totalPlays++;
auto& grade = schema.grades.at(gradeID);
grade.sound.play();
if (text.is_interruptible() && grade.pool.is_valid()) text.set(dialogue.get(grade.pool), character);
if (!grade.isFailure)
{
combo++;
score += grade.value;
if (score >= schema.rewardScore && !isRewardScoreAchieved)
{
schema.sounds.rewardScore.play();
isRewardScoreAchieved = true;
for (auto& itemID : itemSchema.skillCheckRewardItemPool)
{
inventory.values[itemID]++;
if (!itemActors.contains(itemID))
{
itemActors[itemID] = Actor(itemSchema.anm2s[itemID], {}, Actor::SET);
itemRects[itemID] = itemActors[itemID].rect();
}
auto rect = itemRects[itemID];
auto rectSize = vec2(rect.z, rect.w);
auto previewScale = (rectSize.x <= 0.0f || rectSize.y <= 0.0f || size.x <= 0.0f || size.y <= 0.0f ||
!std::isfinite(rectSize.x) || !std::isfinite(rectSize.y))
? 0.0f
: std::min(size.x / rectSize.x, size.y / rectSize.y);
previewScale = std::min(1.0f, previewScale);
auto previewSize = rectSize * previewScale;
auto minX = position.x;
auto maxX = position.x + size.x - previewSize.x;
auto spawnX = minX >= maxX ? position.x : math::random_in_range(minX, maxX);
auto spawnY = position.y - previewSize.y - math::random_in_range(0.0f, size.y);
items.push_back({itemID, ImVec2(spawnX, spawnY), 0.0f});
}
auto toastMessagePosition =
ImVec2(barMin.x - ImGui::CalcTextSize("Fantastic score!\nCongratulations!").x -
ImGui::GetTextLineHeightWithSpacing(),
lineMin.y + (ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y));
toasts.emplace_back("Fantastic score! Congratulations!", toastMessagePosition, schema.endTimerMax,
schema.endTimerMax);
}
if (score > highScore)
{
highScore = score;
if (isHighScoreAchieved && !isHighScoreAchievedThisRun)
{
isHighScoreAchievedThisRun = true;
schema.sounds.highScore.play();
auto toastMessagePosition =
ImVec2(barMin.x - ImGui::CalcTextSize("High Score!").x - ImGui::GetTextLineHeightWithSpacing(),
lineMin.y + ImGui::GetTextLineHeightWithSpacing());
toasts.emplace_back("High Score!", toastMessagePosition, schema.endTimerMax, schema.endTimerMax);
}
}
if (combo > bestCombo) bestCombo = combo;
auto rewardBonus = (schema.rewardGradeBonus * score) + (schema.rewardGradeBonus * grade.value);
while (rewardBonus > 0.0f)
{
const resource::xml::Item::Pool* pool{};
int rewardID{-1};
int rarityID{-1};
auto chanceBonus = std::max(1.0f, (float)grade.value);
for (auto& id : itemSchema.rarityIDsSortedByChance)
{
auto& rarity = itemSchema.rarities[id];
if (rarity.chance <= 0.0f) continue;
if (math::random_percent_roll(rarity.chance * chanceBonus))
{
pool = &itemSchema.pools[id];
rarityID = id;
break;
}
}
if (pool && !pool->empty())
{
rewardID = (*pool)[(int)math::random_roll((float)pool->size())];
auto& rarity = itemSchema.rarities.at(rarityID);
rarity.sound.play();
inventory.values[rewardID]++;
if (!itemActors.contains(rewardID))
{
itemActors[rewardID] = Actor(itemSchema.anm2s[rewardID], {}, Actor::SET);
itemRects[rewardID] = itemActors[rewardID].rect();
}
auto rect = itemRects[rewardID];
auto rectSize = vec2(rect.z, rect.w);
auto previewScale = (rectSize.x <= 0.0f || rectSize.y <= 0.0f || size.x <= 0.0f || size.y <= 0.0f ||
!std::isfinite(rectSize.x) || !std::isfinite(rectSize.y))
? 0.0f
: std::min(size.x / rectSize.x, size.y / rectSize.y);
previewScale = std::min(1.0f, previewScale);
auto previewSize = rectSize * previewScale;
auto minX = position.x;
auto maxX = position.x + size.x - previewSize.x;
auto spawnX = minX >= maxX ? position.x : math::random_in_range(minX, maxX);
auto spawnY = position.y - previewSize.y - math::random_in_range(0.0f, size.y);
items.push_back({rewardID, ImVec2(spawnX, spawnY), 0.0f});
}
rewardBonus -= 1.0f;
}
}
else
{
score = 0;
combo = 0;
if (isHighScoreAchieved) schema.sounds.highScoreLoss.play();
if (highScore > 0) isHighScoreAchieved = true;
isRewardScoreAchieved = false;
isHighScoreAchievedThisRun = true;
highScoreStart = highScore;
isGameOver = true;
}
endTimerMax = grade.isFailure ? schema.endTimerFailureMax : schema.endTimerMax;
isActive = false;
endTimer = endTimerMax;
queuedChallenge = challenge_generate(character);
auto string = grade.isFailure ? grade.name : std::format("{} (+{})", 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);
}
}
else
{
endTimer--;
if (endTimer <= 0)
{
challenge = queuedChallenge;
tryValue = challenge.tryValue;
isActive = true;
isGameOver = false;
}
}
for (int i = 0; i < (int)toasts.size(); i++)
{
auto& toastMessage = toasts[i];
toastMessage.position.y -= TOAST_MESSAGE_SPEED;
auto textColor = ImGui::GetStyleColorVec4(ImGuiCol_Text);
textColor.w = ((float)toastMessage.time / toastMessage.timeMax);
drawList->AddText(toastMessage.position, ImGui::GetColorU32(textColor), toastMessage.message.c_str());
toastMessage.time--;
if (toastMessage.time <= 0) toasts.erase(toasts.begin() + i--);
}
auto gravity = ITEM_FALL_GRAVITY;
auto windowMin = position;
auto windowMax = ImVec2(position.x + size.x, position.y + size.y);
ImGui::PushClipRect(windowMin, windowMax, true);
for (int i = 0; i < (int)items.size(); i++)
{
auto& fallingItem = items[i];
if (!itemActors.contains(fallingItem.id))
{
items.erase(items.begin() + i--);
continue;
}
auto rect = itemRects[fallingItem.id];
auto rectSize = vec2(rect.z, rect.w);
auto previewScale = (rectSize.x <= 0.0f || rectSize.y <= 0.0f || size.x <= 0.0f || size.y <= 0.0f ||
!std::isfinite(rectSize.x) || !std::isfinite(rectSize.y))
? 0.0f
: std::min(size.x / rectSize.x, size.y / rectSize.y);
previewScale = std::min(1.0f, previewScale);
auto previewSize = rectSize * previewScale;
auto canvasSize = ivec2(std::max(1.0f, previewSize.x), std::max(1.0f, previewSize.y));
if (!itemCanvases.contains(fallingItem.id))
itemCanvases.emplace(fallingItem.id, Canvas(canvasSize, Canvas::FLIP));
auto& canvas = itemCanvases[fallingItem.id];
canvas.zoom = math::to_percent(previewScale);
canvas.pan = vec2(rect.x, rect.y);
canvas.bind();
canvas.size_set(canvasSize);
canvas.clear();
itemActors[fallingItem.id].render(resources.shaders[shader::TEXTURE], resources.shaders[shader::RECT], canvas);
canvas.unbind();
auto min = fallingItem.position;
auto max = ImVec2(fallingItem.position.x + previewSize.x, fallingItem.position.y + previewSize.y);
drawList->AddImage(canvas.texture, min, max);
fallingItem.velocity += gravity * io.DeltaTime;
fallingItem.position.y += fallingItem.velocity * io.DeltaTime;
if (fallingItem.position.y > position.y + size.y) items.erase(items.begin() + i--);
}
ImGui::PopClipRect();
}
}

View File

@@ -0,0 +1,87 @@
#pragma once
#include "../../../render/canvas.hpp"
#include "../../../entity/actor.hpp"
#include "../../../entity/character.hpp"
#include "../../../resources.hpp"
#include "../inventory.hpp"
#include "../text.hpp"
#include <imgui.h>
#include <map>
#include <unordered_map>
#include <vector>
namespace game::state::play
{
class SkillCheck
{
public:
struct Range
{
float min{};
float max{};
};
struct Challenge
{
Range range{};
float speed{};
float tryValue{};
int level{};
};
struct Toast
{
std::string message{};
ImVec2 position;
int time{};
int timeMax{};
};
struct Item
{
int id{-1};
ImVec2 position{};
float velocity{};
};
Challenge challenge{};
Challenge queuedChallenge{};
float tryValue{};
int score{};
int combo{};
int endTimer{};
int endTimerMax{};
int highScoreStart{};
int bestCombo{};
int highScore{};
int totalPlays{};
std::map<int, int> gradeCounts{};
bool isActive{true};
bool isRewardScoreAchieved{false};
bool isHighScoreAchieved{false};
bool isHighScoreAchievedThisRun{false};
bool isGameOver{};
std::vector<Toast> toasts{};
std::vector<Item> items{};
std::unordered_map<int, entity::Actor> itemActors{};
std::unordered_map<int, glm::vec4> itemRects{};
std::unordered_map<int, Canvas> itemCanvases{};
SkillCheck() = default;
SkillCheck(entity::Character&);
Challenge challenge_generate(entity::Character&);
void tick();
void update(Resources&, entity::Character&, Inventory&, Text&);
float accuracy_score_get(entity::Character&);
};
}

View File

@@ -0,0 +1,26 @@
#include "area_manager.hpp"
#include <imgui.h>
using namespace game::resource;
using namespace game::util;
namespace game::state::play
{
int AreaManager::get(entity::Character& character)
{
auto& data = character.data;
auto& schema = data.areaSchema;
if (schema.areas.empty()) return -1;
auto size = (int)data.stages.size();
for (int i = 0; i < size; i++)
{
auto& stage = data.stages[size - i - 1];
if (stage.areaID != -1) return stage.areaID;
}
return -1;
}
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "../../entity/character.hpp"
namespace game::state::play
{
class AreaManager
{
public:
int get(entity::Character&);
};
}

Some files were not shown because too many files have changed in this diff Show More