Compare commits
44 Commits
1f1ac0db4d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fb6f902f28 | |||
| b060784bb7 | |||
| 9a2e03b146 | |||
| b81296a4f2 | |||
| 554d6198fd | |||
| 70b5277f87 | |||
| 3375f56492 | |||
| 03ee76e0a5 | |||
| a7f11f8842 | |||
| 3cec6a5541 | |||
| 1f0a1d4f47 | |||
| 2d230ecd2e | |||
| 2a58c3b24b | |||
| 475fb5a847 | |||
| 06d2cbdc12 | |||
| 6ed9a15177 | |||
|
|
a9121b58a0 | ||
|
|
83ba5699ac | ||
| 841ff371da | |||
| 0b70bab618 | |||
| 94db77e8da | |||
| 154bccb3d5 | |||
| 0f855f7125 | |||
| 2aaf6dcf75 | |||
| f594ba3889 | |||
| 45ee9a7d11 | |||
| b3c097be22 | |||
| 68d5301735 | |||
| d016768ca9 | |||
| f11436abaa | |||
| 9dc34c72d4 | |||
| ac41e4f31d | |||
| 00b3a146d5 | |||
| 0d9d8ff96f | |||
| bc8fe78fce | |||
| 3817f1cc39 | |||
| acb1505308 | |||
| e2a2d2c464 | |||
| cf8864b90b | |||
| 04765ad058 | |||
| d749508c1c | |||
| c116b5c075 | |||
| 17f3348e94 | |||
| 8b2edd1359 |
4
.clangd
@@ -1,3 +1,3 @@
|
||||
CompileFlags:
|
||||
CompilationDatabase: build
|
||||
Add: [-std=c++20]
|
||||
CompilationDatabase: out/build/linux-debug
|
||||
Add: [-std=gnu++23]
|
||||
|
||||
41
.github/workflows/build.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Game
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: "recursive"
|
||||
|
||||
- name: "Install dependencies"
|
||||
run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y \
|
||||
gnome-desktop-testing libasound2-dev libpulse-dev libaudio-dev libjack-dev libsndio-dev \
|
||||
libusb-1.0-0-dev libx11-dev libxext-dev libxrandr-dev libxcursor-dev libxfixes-dev libxi-dev \
|
||||
libxss-dev libxtst-dev libwayland-dev libxkbcommon-dev libdrm-dev libgbm-dev libgl1-mesa-dev \
|
||||
libgles2-mesa-dev libegl1-mesa-dev libdbus-1-dev libibus-1.0-dev libudev-dev fcitx-libs-dev
|
||||
|
||||
- name: CMake Build
|
||||
run: |
|
||||
mkdir build
|
||||
cmake -S . -B ./build -DSDL_UNIX_CONSOLE_BUILD=ON
|
||||
make -C build
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: linux-build
|
||||
path: build/snivy
|
||||
|
||||
2
.gitignore
vendored
@@ -2,3 +2,5 @@ build/
|
||||
build-web/
|
||||
resources/
|
||||
release/
|
||||
out/
|
||||
external/
|
||||
6
.gitmodules
vendored
@@ -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
|
||||
|
||||
41
.vscode/launch.json
vendored
@@ -2,13 +2,14 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug",
|
||||
"name": "Debug (CMake Debug preset)",
|
||||
"type": "cppdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/build/snivy",
|
||||
"preLaunchTask": "cmake: build debug",
|
||||
"program": "${workspaceFolder}/out/build/linux-debug/bin/Debug/snivy",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}/build",
|
||||
"cwd": "${workspaceFolder}/out/build/linux-debug/bin/Debug",
|
||||
"environment": [],
|
||||
"externalConsole": false,
|
||||
"MIMode": "gdb",
|
||||
@@ -22,7 +23,39 @@
|
||||
"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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
155
.vscode/tasks.json
vendored
@@ -2,9 +2,26 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build",
|
||||
"label": "cmake: configure debug",
|
||||
"type": "shell",
|
||||
"command": "cmake --build build",
|
||||
"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
|
||||
@@ -14,31 +31,135 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "start-wasm-devserver",
|
||||
"label": "cmake: configure release",
|
||||
"type": "shell",
|
||||
"command": "${workspaceFolder}/scripts/start_wasm_dev.sh",
|
||||
"presentation": {
|
||||
"reveal": "silent",
|
||||
"panel": "dedicated"
|
||||
"linux": {
|
||||
"command": "cmake --preset linux-release"
|
||||
},
|
||||
"windows": {
|
||||
"command": "cmake --preset x64-Release"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "stop-wasm-devserver",
|
||||
"label": "cmake: build release",
|
||||
"type": "shell",
|
||||
"command": "${workspaceFolder}/scripts/stop_wasm_dev.sh",
|
||||
"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": []
|
||||
},
|
||||
{
|
||||
"type": "cmake",
|
||||
"label": "CMake: build",
|
||||
"command": "build",
|
||||
"targets": [
|
||||
"[N/A - Select Kit]"
|
||||
"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"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"detail": "CMake template build task"
|
||||
"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": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
150
CMakeLists.txt
@@ -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")
|
||||
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 "-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 "-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
|
||||
@@ -77,10 +109,17 @@ set (TINYXML2_SOURCES ${TINYXML2_DIR}/tinyxml2.cpp)
|
||||
file(GLOB PROJECT_SRC CONFIGURE_DEPENDS
|
||||
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/*.h
|
||||
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}/*")
|
||||
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
|
||||
add_custom_target(copy_resources ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${PROJECT_RESOURCES_DIR}" "${PROJECT_RESOURCES_BINARY_DIR}"
|
||||
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
@@ -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
@@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
602
MODDING.md
Normal 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? :^) )
|
||||
@@ -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.
|
||||
55
cmake/copy_resources.cmake
Normal 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()
|
||||
28
cmake/create_index_zip.cmake
Normal 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()
|
||||
47
cmake/create_windows_zip.cmake
Normal 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
1
external/physfs
vendored
Submodule
8881
include/stb_image.h
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 71 KiB |
@@ -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);
|
||||
}
|
||||
}
|
||||
185
src/character.h
@@ -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&);
|
||||
};
|
||||
}
|
||||
@@ -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()); }
|
||||
}
|
||||
19
src/cursor.h
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "types.h"
|
||||
|
||||
namespace game
|
||||
{
|
||||
class GameData
|
||||
{
|
||||
public:
|
||||
MeasurementSystem measurementSystem{MeasurementSystem::METRIC};
|
||||
int volume{100};
|
||||
};
|
||||
}
|
||||
129
src/item.cpp
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
241
src/item.h
@@ -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);
|
||||
};
|
||||
}
|
||||
147
src/loader.cpp
@@ -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...");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
10
src/main.cpp
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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; }
|
||||
};
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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&);
|
||||
};
|
||||
}
|
||||
@@ -1,73 +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::set_gain(float gain)
|
||||
void Audio::volume_set(float volume)
|
||||
{
|
||||
auto mixer = mixer_get();
|
||||
MIX_SetMasterGain(mixer, gain);
|
||||
}
|
||||
|
||||
void Audio::retain()
|
||||
{
|
||||
if (refCount) ++(*refCount);
|
||||
}
|
||||
|
||||
void Audio::release()
|
||||
{
|
||||
if (refCount)
|
||||
{
|
||||
if (--(*refCount) == 0)
|
||||
{
|
||||
if (internal) MIX_DestroyAudio(internal);
|
||||
delete refCount;
|
||||
}
|
||||
refCount = nullptr;
|
||||
}
|
||||
internal = nullptr;
|
||||
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)
|
||||
{
|
||||
std::cout << "Failed to initialize audio: '" << path.string() << "'\n";
|
||||
if (!path.is_valid())
|
||||
{
|
||||
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)
|
||||
@@ -76,8 +125,7 @@ namespace game::resource
|
||||
{
|
||||
unload();
|
||||
internal = other.internal;
|
||||
refCount = other.refCount;
|
||||
retain();
|
||||
track = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
@@ -87,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;
|
||||
}
|
||||
@@ -105,7 +150,7 @@ namespace game::resource
|
||||
MIX_DestroyTrack(track);
|
||||
track = nullptr;
|
||||
}
|
||||
release();
|
||||
internal.reset();
|
||||
}
|
||||
|
||||
void Audio::play(bool isLoop)
|
||||
@@ -126,7 +171,7 @@ namespace game::resource
|
||||
if (!track) return;
|
||||
}
|
||||
|
||||
MIX_SetTrackAudio(track, internal);
|
||||
MIX_SetTrackAudio(track, internal.get());
|
||||
|
||||
SDL_PropertiesID options = 0;
|
||||
|
||||
@@ -149,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; }
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
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,6 +29,6 @@ namespace game::resource
|
||||
void play(bool isLoop = false);
|
||||
void stop();
|
||||
bool is_playing() const;
|
||||
static void set_gain(float vol);
|
||||
static void volume_set(float volume);
|
||||
};
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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*);
|
||||
};
|
||||
}
|
||||
@@ -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; };
|
||||
}
|
||||
@@ -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
@@ -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();
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,26 +101,27 @@ 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;
|
||||
}
|
||||
|
||||
Texture::Texture(const std::filesystem::path& path)
|
||||
void Texture::init(const uint8_t* data)
|
||||
{
|
||||
if (auto data = stbi_load(path.c_str(), &size.x, &size.y, nullptr, CHANNELS); data)
|
||||
{
|
||||
glGenTextures(1, &id);
|
||||
glBindTexture(GL_TEXTURE_2D, id);
|
||||
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);
|
||||
@@ -115,18 +129,76 @@ namespace game::resource
|
||||
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";
|
||||
|
||||
idShared = texture_id_make(newId);
|
||||
id = newId;
|
||||
}
|
||||
|
||||
Texture::Texture(const std::filesystem::path& path)
|
||||
{
|
||||
auto pathString = path.string();
|
||||
auto key = std::string("fs:") + pathString;
|
||||
if (cache_get(key, idShared, id, size, channels))
|
||||
{
|
||||
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
|
||||
{
|
||||
id = 0;
|
||||
size = {};
|
||||
channels = 0;
|
||||
refCount = nullptr;
|
||||
std::cout << "Failed to initialize texture: '" << path.string() << "'\n";
|
||||
logger.error(std::format("Failed to initialize texture: {} ({})", pathString, SDL_GetError()));
|
||||
}
|
||||
|
||||
Texture::Texture(const physfs::Path& path)
|
||||
{
|
||||
if (!path.is_valid())
|
||||
{
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{};
|
||||
};
|
||||
}
|
||||
12
src/resource/xml/animation_entry.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
23
src/resource/xml/animation_entry.hpp
Normal 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
@@ -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", ®ionID);
|
||||
xml::query_string_attribute(regionChild, "Name", ®ion.name);
|
||||
regionChild->QueryFloatAttribute("XCrop", ®ion.crop.x);
|
||||
regionChild->QueryFloatAttribute("YCrop", ®ion.crop.y);
|
||||
regionChild->QueryFloatAttribute("Width", ®ion.size.x);
|
||||
regionChild->QueryFloatAttribute("Height", ®ion.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", ®ion.pivot.x);
|
||||
regionChild->QueryFloatAttribute("YPivot", ®ion.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
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
}
|
||||
241
src/resource/xml/character.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
145
src/resource/xml/character.hpp
Normal 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();
|
||||
};
|
||||
}
|
||||
69
src/resource/xml/character_preview.cpp
Normal 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; }
|
||||
}
|
||||
44
src/resource/xml/character_preview.hpp
Normal 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;
|
||||
};
|
||||
}
|
||||
54
src/resource/xml/cursor.cpp
Normal 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; };
|
||||
}
|
||||
38
src/resource/xml/cursor.hpp
Normal 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;
|
||||
};
|
||||
}
|
||||
172
src/resource/xml/dialogue.cpp
Normal 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()); }
|
||||
}
|
||||
90
src/resource/xml/dialogue.hpp
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
};
|
||||
}
|
||||
77
src/resource/xml/settings.cpp
Normal 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
|
||||
}
|
||||
}
|
||||
37
src/resource/xml/settings.hpp
Normal 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;
|
||||
};
|
||||
}
|
||||
69
src/resource/xml/skill_check.cpp
Normal 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; };
|
||||
}
|
||||
56
src/resource/xml/skill_check.hpp
Normal 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;
|
||||
};
|
||||
}
|
||||
18
src/resource/xml/sound_entry.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
24
src/resource/xml/sound_entry.hpp
Normal 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
@@ -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
@@ -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&);
|
||||
}
|
||||
@@ -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,19 +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]);
|
||||
|
||||
for (int i = 0; i < anm2::COUNT; i++)
|
||||
anm2s[i] = Anm2(anm2::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;
|
||||
}
|
||||
|
||||
void Resources::sound_play(audio::Type type) { audio[type].play(); }
|
||||
|
||||
void Resources::set_audio_gain(float vol) {
|
||||
Audio::set_gain(vol);
|
||||
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::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"); }
|
||||
}
|
||||
129
src/resources.h
@@ -1,129 +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);
|
||||
void set_audio_gain(float vol);
|
||||
};
|
||||
}
|
||||
32
src/resources.hpp
Normal 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);
|
||||
};
|
||||
}
|
||||
455
src/state.cpp
@@ -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,402 +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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
SDL_Event event;
|
||||
|
||||
while (SDL_PollEvent(&event))
|
||||
{
|
||||
ImGui_ImplSDL3_ProcessEvent(&event);
|
||||
if (event.type == SDL_EVENT_QUIT) isRunning = false;
|
||||
}
|
||||
if (!isRunning) return;
|
||||
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
auto style = ImGui::GetStyle();
|
||||
|
||||
if (textWindow.isFlagActivated)
|
||||
{
|
||||
switch (textWindow.flag)
|
||||
{
|
||||
case Dialogue::Entry::ACTIVATE_WINDOWS:
|
||||
isInfo = true;
|
||||
isMainMenu = true;
|
||||
case SELECT:
|
||||
select.tick();
|
||||
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;
|
||||
case PLAY:
|
||||
play.tick(resources);
|
||||
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)
|
||||
void State::update()
|
||||
{
|
||||
if (items.size() > Item::DEPLOYED_MAX)
|
||||
#ifndef __EMSCRIPTEN__
|
||||
SDL_GetWindowSize(window, &resources.settings.windowSize.x, &resources.settings.windowSize.y);
|
||||
#endif
|
||||
|
||||
SDL_Event event;
|
||||
|
||||
while (SDL_PollEvent(&event))
|
||||
{
|
||||
inventory.isQueued = false;
|
||||
inventory.queuedItemType = Item::NONE;
|
||||
resources.sound_play(audio::MISS);
|
||||
ImGui_ImplSDL3_ProcessEvent(&event);
|
||||
if (event.type == SDL_EVENT_QUIT)
|
||||
{
|
||||
if (type == PLAY) play.exit(resources);
|
||||
isRunning = false;
|
||||
}
|
||||
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.adjust_item(type, -1);
|
||||
type = Item::NONE;
|
||||
inventory.isQueued = false;
|
||||
if (!isRunning) return;
|
||||
}
|
||||
ImGui_ImplOpenGL3_NewFrame();
|
||||
ImGui_ImplSDL3_NewFrame();
|
||||
|
||||
ImGui::NewFrame();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/* Item */
|
||||
Item::hoveredItem = nullptr;
|
||||
Item::heldItemPrevious = Item::heldItem;
|
||||
bool isSpeak{};
|
||||
std::vector<int>* dialogueIDs{};
|
||||
|
||||
for (int i = 0; i < items.size(); i++)
|
||||
auto isHideCursor = type == PLAY;
|
||||
if (isHideCursor != isCursorHidden)
|
||||
{
|
||||
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.adjust_item(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);
|
||||
}
|
||||
}
|
||||
|
||||
if (isHideCursor)
|
||||
SDL_HideCursor();
|
||||
cursor.update();
|
||||
else
|
||||
SDL_ShowCursor();
|
||||
isCursorHidden = isHideCursor;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
54
src/state.h
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
};
|
||||
};
|
||||
433
src/state/play/arcade/skill_check.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
87
src/state/play/arcade/skill_check.hpp
Normal 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&);
|
||||
};
|
||||
}
|
||||
26
src/state/play/area_manager.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||