Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fcf8c4fa5 | ||
|
|
889c6d465b | ||
|
|
8df016c0a4 | ||
|
|
2fe2ee6c2a | ||
|
|
cf714d4133 | ||
|
|
255181cf31 | ||
|
|
ef966a2197 | ||
|
|
24c963c0a5 | ||
|
|
6cfd47e80d | ||
|
|
0be23ca291 | ||
|
|
8c4eeb9793 |
No files matched your search
@@ -0,0 +1,47 @@
|
||||
# cmake/FetchDependenciesServer.cmake
|
||||
#
|
||||
# Downloads only what the headless GameServer needs (Eigen + Boost headers).
|
||||
# Skips anything already present in thirdparty/. Works on Windows and Linux.
|
||||
|
||||
set(THIRDPARTY_DIR "${CMAKE_CURRENT_LIST_DIR}/../thirdparty")
|
||||
file(MAKE_DIRECTORY "${THIRDPARTY_DIR}")
|
||||
|
||||
macro(server_check_and_download URL ARCHIVE_NAME EXTRACTED_DIR_NAME CHECK_FILE)
|
||||
set(_archive_path "${THIRDPARTY_DIR}/${ARCHIVE_NAME}")
|
||||
set(_src_path "${THIRDPARTY_DIR}/${EXTRACTED_DIR_NAME}")
|
||||
|
||||
if(NOT EXISTS "${_src_path}/${CHECK_FILE}")
|
||||
if(NOT EXISTS "${_archive_path}")
|
||||
message(STATUS "Downloading ${ARCHIVE_NAME}...")
|
||||
file(DOWNLOAD "${URL}" "${_archive_path}"
|
||||
SHOW_PROGRESS STATUS _dl_status)
|
||||
list(GET _dl_status 0 _dl_code)
|
||||
if(NOT _dl_code EQUAL 0)
|
||||
file(REMOVE "${_archive_path}")
|
||||
message(FATAL_ERROR "Failed to download ${URL}: ${_dl_status}")
|
||||
endif()
|
||||
endif()
|
||||
message(STATUS "Extracting ${ARCHIVE_NAME}...")
|
||||
execute_process(
|
||||
COMMAND ${CMAKE_COMMAND} -E tar xf "${_archive_path}"
|
||||
WORKING_DIRECTORY "${THIRDPARTY_DIR}"
|
||||
RESULT_VARIABLE _tar_res)
|
||||
if(NOT _tar_res EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to extract ${_archive_path}")
|
||||
endif()
|
||||
endif()
|
||||
endmacro()
|
||||
|
||||
# Eigen: only needed if no system install is available.
|
||||
find_package(Eigen3 3.3 CONFIG QUIET)
|
||||
if(NOT TARGET Eigen3::Eigen)
|
||||
server_check_and_download(
|
||||
"https://gitlab.com/libeigen/eigen/-/archive/5.0.0/eigen-5.0.0.zip"
|
||||
"eigen-5.0.0.zip" "eigen-5.0.0" "signature_of_eigen3_matrix_library")
|
||||
endif()
|
||||
|
||||
# Boost (header-only usage: asio + beast). Use bundled copy always so the
|
||||
# version matches the Windows build.
|
||||
server_check_and_download(
|
||||
"https://archives.boost.io/release/1.90.0/source/boost_1_90_0.zip"
|
||||
"boost_1_90_0.zip" "boost_1_90_0" "boost/asio.hpp")
|
||||
@@ -246,7 +246,7 @@ set(EMSCRIPTEN_LINK_FLAGS
|
||||
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_uni_interior.lua@resources/start_uni_interior.lua"
|
||||
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_uni_exterior.lua@resources/start_uni_exterior.lua"
|
||||
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_dorm.lua@resources/start_dorm.lua"
|
||||
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_dorm.lua@resources/start_battle_zone.lua"
|
||||
"--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/start_battle_zone.lua@resources/start_battle_zone.lua"
|
||||
)
|
||||
|
||||
# Применяем настройки линковки
|
||||
|
||||
+34
-3
@@ -5,17 +5,28 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||
<title>Shadow Over Bishkek</title>
|
||||
<style>
|
||||
:root {
|
||||
/* Visible viewport size, kept in sync by syncViewport() below.
|
||||
100vh on mobile is the *largest* viewport (URL bar hidden) and
|
||||
would push the bottom of the canvas off-screen. */
|
||||
--app-w: 100%;
|
||||
--app-h: 100dvh;
|
||||
}
|
||||
body, html {
|
||||
margin: 0; padding: 0; width: 100%; height: 100%;
|
||||
overflow: hidden; background-color: #000;
|
||||
position: fixed;
|
||||
position: fixed; top: 0; left: 0;
|
||||
}
|
||||
#canvas {
|
||||
display: block;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100vw; height: 100vh;
|
||||
width: var(--app-w); height: var(--app-h);
|
||||
border: none;
|
||||
touch-action: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
|
||||
@@ -49,8 +60,28 @@
|
||||
loadGameScript();
|
||||
});
|
||||
|
||||
function syncViewport() {
|
||||
var vv = window.visualViewport;
|
||||
var w = (vv && vv.width) || window.innerWidth;
|
||||
var h = (vv && vv.height) || window.innerHeight;
|
||||
var root = document.documentElement;
|
||||
root.style.setProperty('--app-w', Math.round(w) + 'px');
|
||||
root.style.setProperty('--app-h', Math.round(h) + 'px');
|
||||
}
|
||||
syncViewport();
|
||||
// Registered before the game script loads, so the CSS size is updated
|
||||
// before the engine's own resize handler reads it.
|
||||
window.addEventListener('resize', syncViewport);
|
||||
if (window.visualViewport) {
|
||||
window.visualViewport.addEventListener('resize', function () {
|
||||
syncViewport();
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener("orientationchange", function() {
|
||||
setTimeout(() => {
|
||||
syncViewport();
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
}, 200);
|
||||
});
|
||||
|
||||
@@ -131,8 +131,14 @@ set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT S
|
||||
# include-пути проекта
|
||||
target_include_directories(ShadowOverBishkekDemo PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../src"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build/Release/include"
|
||||
)
|
||||
|
||||
#target_link_directories(ShadowOverBishkekDemo PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build/Release/lib")
|
||||
|
||||
target_link_directories(ShadowOverBishkekDemo PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build_debug/Debug/lib")
|
||||
|
||||
|
||||
set_target_properties(ShadowOverBishkekDemo PROPERTIES
|
||||
OUTPUT_NAME "ShadowOverBishkekDemo"
|
||||
)
|
||||
@@ -162,6 +168,7 @@ target_link_libraries(ShadowOverBishkekDemo PRIVATE
|
||||
sol2_external_lib
|
||||
SDL2_mixer_external_lib
|
||||
openssl_ssl_external_lib
|
||||
Clipper2.lib
|
||||
)
|
||||
|
||||
# Линкуем OpenGL (Windows)
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"y": 10,
|
||||
"width": 160,
|
||||
"height": 36,
|
||||
"text": "v. 1.0.7",
|
||||
"text": "v. 1.1.2",
|
||||
"fontSize": 24,
|
||||
"textCentered": false,
|
||||
"topAligned": true,
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
"y": 10,
|
||||
"width": 160,
|
||||
"height": 36,
|
||||
"text": "v. 1.0.7",
|
||||
"text": "v. 1.1.2",
|
||||
"fontSize": 24,
|
||||
"textCentered": false,
|
||||
"topAligned": true,
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"y": 10,
|
||||
"width": 160,
|
||||
"height": 36,
|
||||
"text": "v. 1.0.7",
|
||||
"text": "v. 1.1.2",
|
||||
"fontSize": 24,
|
||||
"textCentered": false,
|
||||
"topAligned": true,
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
"y": 10,
|
||||
"width": 160,
|
||||
"height": 36,
|
||||
"text": "v. 1.0.7",
|
||||
"text": "v. 1.1.2",
|
||||
"fontSize": 24,
|
||||
"textCentered": false,
|
||||
"topAligned": true,
|
||||
|
||||
+33
-17
@@ -9,6 +9,7 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(GAME_ROOT "${CMAKE_CURRENT_LIST_DIR}/..")
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
include("${GAME_ROOT}/cmake/FetchDependenciesServer.cmake")
|
||||
find_package(Eigen3 3.3 CONFIG QUIET)
|
||||
if(NOT TARGET Eigen3::Eigen)
|
||||
set(EIGEN_BUNDLED_DIR "${GAME_ROOT}/thirdparty/eigen-5.0.0")
|
||||
@@ -105,24 +106,32 @@ endif()
|
||||
add_executable(GameServer src/main.cpp README.md)
|
||||
source_group("Server" FILES src/main.cpp README.md)
|
||||
target_link_libraries(GameServer PRIVATE game_server_core)
|
||||
set(BOOST_BUNDLED_DIR "${GAME_ROOT}/thirdparty/boost_1_90_0")
|
||||
if(NOT EXISTS "${BOOST_BUNDLED_DIR}/boost/asio.hpp")
|
||||
message(FATAL_ERROR
|
||||
"Boost was not found at thirdparty/boost_1_90_0 and could not be "
|
||||
"downloaded.")
|
||||
endif()
|
||||
target_sources(GameServer PRIVATE
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.cpp"
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.h"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.cpp"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.h")
|
||||
target_include_directories(GameServer PRIVATE "${BOOST_BUNDLED_DIR}")
|
||||
target_compile_definitions(GameServer PRIVATE NETWORK)
|
||||
if(WIN32)
|
||||
set(BOOST_BUNDLED_DIR "${GAME_ROOT}/thirdparty/boost_1_90_0")
|
||||
if(NOT EXISTS "${BOOST_BUNDLED_DIR}/boost/asio.hpp")
|
||||
message(FATAL_ERROR
|
||||
"Boost was not found at thirdparty/boost_1_90_0. Configure the "
|
||||
"graphical project once to populate it.")
|
||||
endif()
|
||||
target_sources(GameServer PRIVATE
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.cpp"
|
||||
"${GAME_ROOT}/src/game_server/NetworkProtocol.h"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.cpp"
|
||||
"${GAME_ROOT}/src/game_server/WebsocketGameServer.h")
|
||||
target_include_directories(GameServer PRIVATE "${BOOST_BUNDLED_DIR}")
|
||||
target_compile_definitions(
|
||||
GameServer PRIVATE NETWORK WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||
GameServer PRIVATE WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||
target_link_libraries(GameServer PRIVATE ws2_32 mswsock)
|
||||
endif()
|
||||
|
||||
# The server reads configs from ./resources relative to its working directory,
|
||||
# so keep a copy next to the binary after every build.
|
||||
add_custom_command(TARGET GameServer POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${GAME_ROOT}/resources" "$<TARGET_FILE_DIR:GameServer>/resources"
|
||||
COMMENT "Copying resources next to GameServer")
|
||||
|
||||
include(CTest)
|
||||
if(BUILD_TESTING)
|
||||
add_executable(
|
||||
@@ -141,7 +150,8 @@ if(BUILD_TESTING)
|
||||
add_test(
|
||||
NAME GameServer.LocationState
|
||||
COMMAND GameServerStateTests)
|
||||
if(WIN32)
|
||||
# network tests (all platforms)
|
||||
if(TRUE)
|
||||
add_executable(
|
||||
GameServerNetworkProtocolTests
|
||||
tests/NetworkProtocolTests.cpp
|
||||
@@ -169,8 +179,14 @@ if(BUILD_TESTING)
|
||||
PROPERTIES COMPILE_OPTIONS "/bigobj")
|
||||
endif()
|
||||
target_link_libraries(
|
||||
GameServerWebsocketLockstepTests PRIVATE
|
||||
game_server_core ws2_32 mswsock)
|
||||
GameServerWebsocketLockstepTests PRIVATE game_server_core)
|
||||
if(WIN32)
|
||||
target_compile_definitions(
|
||||
GameServerWebsocketLockstepTests PRIVATE
|
||||
WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
|
||||
target_link_libraries(
|
||||
GameServerWebsocketLockstepTests PRIVATE ws2_32 mswsock)
|
||||
endif()
|
||||
add_test(
|
||||
NAME GameServer.WebsocketLockstep
|
||||
COMMAND GameServerWebsocketLockstepTests)
|
||||
@@ -180,7 +196,7 @@ if(BUILD_TESTING)
|
||||
COMMAND GameServer --ticks 2 --no-sleep)
|
||||
add_test(
|
||||
NAME GameServer.BattleZoneSharedPlayer
|
||||
COMMAND GameServer --campaign battle_zone --location uni_exterior
|
||||
COMMAND GameServer --campaign battle_zone --location park_korea
|
||||
--ticks 2 --no-sleep)
|
||||
set_tests_properties(
|
||||
GameServer.BattleZoneSharedPlayer
|
||||
|
||||
+123
-62
@@ -1,11 +1,37 @@
|
||||
# Headless GameServer
|
||||
|
||||
This target runs the authoritative `InternalGameServer` without SDL, OpenGL,
|
||||
audio, Lua, or rendering dependencies. On Windows it exposes a deterministic
|
||||
lockstep session through a Boost.Beast WebSocket listener.
|
||||
audio, Lua, or rendering dependencies. It exposes a deterministic lockstep
|
||||
session through a Boost.Beast WebSocket listener (Windows and Linux).
|
||||
|
||||
## Requirements
|
||||
|
||||
- CMake >= 3.16, a C++17 compiler (MSVC, or GCC/Clang on Linux)
|
||||
- Ubuntu: `sudo apt install build-essential cmake git unzip curl`
|
||||
- Internet access on the first configure (see below)
|
||||
|
||||
Eigen and Boost are **downloaded automatically** by
|
||||
`cmake/FetchDependenciesServer.cmake` into `thirdparty/` the first time CMake
|
||||
configures the project (Boost zip is ~200 MB). A system Eigen3 is used if
|
||||
installed; Boost is always taken from `thirdparty/boost_1_90_0` so versions match
|
||||
the Windows build. Nothing else (SDL, zlib, libzip, ...) is needed.
|
||||
|
||||
## Build
|
||||
|
||||
Linux (recommended helper, 1 compile job to stay within low RAM):
|
||||
|
||||
```bash
|
||||
server/scripts/build.sh # JOBS=2 server/scripts/build.sh for more
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
cmake -S server -B server/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build server/build --target GameServer --parallel 1
|
||||
ctest --test-dir server/build --output-on-failure
|
||||
```
|
||||
|
||||
Windows:
|
||||
|
||||
```powershell
|
||||
@@ -14,91 +40,126 @@ cmake --build server/build --config Release --target GameServer
|
||||
ctest --test-dir server/build -C Release --output-on-failure
|
||||
```
|
||||
|
||||
Linux:
|
||||
> **Memory note:** compiling Boost.Beast code can use 1-1.5 GB per translation
|
||||
> unit. On a small VDS keep `--parallel 1`; if the compiler gets OOM-killed, add
|
||||
> temporary swap (`sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile &&
|
||||
> sudo mkswap /swapfile && sudo swapon /swapfile`). The running server itself
|
||||
> needs only a few tens of MB.
|
||||
|
||||
After every build the repository's `resources/` directory is copied next to the
|
||||
binary (`server/build/resources`, or `server/build/Release/resources` on
|
||||
Windows). Content paths are resolved from the process working directory, so the
|
||||
server must be started from the directory that contains `resources/`.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
cmake -S server -B server/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build server/build --target GameServer --parallel
|
||||
ctest --test-dir server/build --output-on-failure
|
||||
server/scripts/run-server.sh
|
||||
```
|
||||
|
||||
Eigen3 must either be installed or be available at
|
||||
`thirdparty/eigen-5.0.0`. The headless target reads ordinary filesystem resources
|
||||
only; ZIP-backed campaign loading belongs to the later content-loading
|
||||
milestone.
|
||||
|
||||
Run until interrupted:
|
||||
This does `cd server/build` and runs
|
||||
|
||||
```bash
|
||||
./server/build/GameServer
|
||||
./GameServer --campaign battle_zone_multiplayer --address 127.0.0.1 --port 8081
|
||||
```
|
||||
|
||||
For a multi-configuration Windows build, the executable is normally
|
||||
`server/build/Release/GameServer.exe`.
|
||||
Environment overrides: `CAMPAIGN`, `LOCATION`, `ADDRESS`, `PORT`, `BUILD_DIR`.
|
||||
|
||||
For smoke tests, `--ticks N` limits execution and `--no-sleep` skips the
|
||||
real-time delay.
|
||||
|
||||
Start the multiplayer lockstep server:
|
||||
Windows equivalent:
|
||||
|
||||
```powershell
|
||||
.\server\build\Release\GameServer.exe --campaign battle_zone_multiplayer --location uni_exterior
|
||||
.\server\build\Release\GameServer.exe --campaign battle_zone_multiplayer
|
||||
```
|
||||
|
||||
The listener defaults to `ws://0.0.0.0:8080`. Launch the Windows graphical game
|
||||
afterward and select `battle_zone_multiplayer`; that campaign connects to
|
||||
`ws://127.0.0.1:8080`. Other campaigns continue to use the in-process server.
|
||||
Command line options: `--campaign ID`, `--location ID`, `--campaigns PATH`
|
||||
(default `resources/config/campaigns.json`), `--address IP` (default `0.0.0.0`),
|
||||
`--port N` (default `8080`), `--ticks N` and `--no-sleep` (smoke tests).
|
||||
|
||||
The server sends one state snapshot when a player connects, then broadcasts one
|
||||
numbered command bundle every 100 ms. Bundles may be empty. Destination input is
|
||||
scheduled two ticks (200 ms) in the future. Tick IDs, rather than server or
|
||||
client wall-clock values, define simulation time.
|
||||
|
||||
If `--location` is omitted, the campaign's configured start location is used.
|
||||
`--campaigns PATH` selects a different campaign registry. Content paths are
|
||||
resolved from the process working directory, so run from the repository root
|
||||
when using the bundled `resources/` tree.
|
||||
## Production deployment (Ubuntu, systemd + nginx)
|
||||
|
||||
Available startup options:
|
||||
The server listens on loopback only (`127.0.0.1:8081`); nginx terminates TLS and
|
||||
proxies WebSocket traffic.
|
||||
|
||||
```text
|
||||
--campaign ID
|
||||
--location ID
|
||||
--campaigns PATH
|
||||
--address IP
|
||||
--port N
|
||||
--tick-ms 100
|
||||
--ticks N
|
||||
--no-sleep
|
||||
### 1. systemd service
|
||||
|
||||
```bash
|
||||
sudo cp server/deploy/shadow-gameserver.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now shadow-gameserver
|
||||
systemctl status shadow-gameserver
|
||||
journalctl -u shadow-gameserver -f # logs
|
||||
```
|
||||
|
||||
Network mode requires a 100 ms tick; another `--tick-ms` value is rejected.
|
||||
The selected campaign's `networkPlayerSlots` lists pre-existing character IDs
|
||||
assigned to connections in order. `networkCompatibility` is a release token
|
||||
that must be changed whenever deterministic content changes; clients reject a
|
||||
different token. The bundled multiplayer Battle Zone has two slots (`player`
|
||||
and `player2`) and rejects additional clients. Disconnecting
|
||||
releases the slot. The bot's random autonomous controller is disabled in
|
||||
multiplayer.
|
||||
The unit caps memory at 512 MB and restarts on failure. After rebuilding run
|
||||
`sudo systemctl restart shadow-gameserver`. Edit `User=`/paths in the unit if the
|
||||
repository lives elsewhere. To run a second server, copy the unit with another
|
||||
`PORT` (and campaign) and add a matching `/server002/` location.
|
||||
|
||||
The graphical client executes presentation behavior after building its runtime
|
||||
`Location`; the headless content pass loads authoritative navigation, NPC,
|
||||
interactive-object, trigger, teleport, taxi, and animation metadata. Lua and
|
||||
dialogue-driven shared multiplayer behavior is not yet supported.
|
||||
### 2. nginx
|
||||
|
||||
## Tests
|
||||
```bash
|
||||
sudo cp server/deploy/nginx-api.shadowoverbishkek.com.conf \
|
||||
/etc/nginx/sites-available/api.shadowoverbishkek.com
|
||||
sudo ln -s /etc/nginx/sites-available/api.shadowoverbishkek.com \
|
||||
/etc/nginx/sites-enabled/
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
CTest covers:
|
||||
`location /server001/` proxies to `http://127.0.0.1:8081/` with WebSocket
|
||||
upgrade headers and a 1 hour read/send timeout (the trailing slashes strip the
|
||||
`/server001` prefix).
|
||||
|
||||
- repeated `LocationGameState::setup()` preserving mutable runtime state;
|
||||
- ticking a prepared location with zero players;
|
||||
- deterministic fixed-100 ms replay behavior;
|
||||
- protocol-v2 parsing, validation, target normalization, state hashing, and
|
||||
two-tick command ordering;
|
||||
- a WebSocket initial-state/slot/destination/tick integration test;
|
||||
- idle server startup;
|
||||
- headless loading of `battle_zone_multiplayer/uni_exterior`.
|
||||
### 3. HTTPS certificate
|
||||
|
||||
The lockstep implementation currently targets identical executable and content
|
||||
builds. Diagnostic state hashes detect divergence, but there is no automatic
|
||||
checkpoint resynchronization or reconnect input history yet.
|
||||
DNS for `api.shadowoverbishkek.com` must point at the VDS first.
|
||||
|
||||
```bash
|
||||
sudo certbot --nginx -d api.shadowoverbishkek.com
|
||||
```
|
||||
|
||||
Certbot issues a Let's Encrypt certificate, adds the `443 ssl` block and an HTTP
|
||||
to HTTPS redirect, and installs auto-renewal (`systemctl list-timers | grep certbot`).
|
||||
|
||||
## Connecting
|
||||
|
||||
Public endpoint: **`wss://api.shadowoverbishkek.com/server001/`**
|
||||
|
||||
Handshake check from any machine (expects `HTTP/1.1 101 Switching Protocols`):
|
||||
|
||||
```bash
|
||||
curl -i -N --max-time 3 \
|
||||
-H "Connection: Upgrade" -H "Upgrade: websocket" \
|
||||
-H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==" \
|
||||
https://api.shadowoverbishkek.com/server001/
|
||||
```
|
||||
|
||||
Or interactively: `npx wscat -c wss://api.shadowoverbishkek.com/server001/`.
|
||||
|
||||
Locally on the VDS, bypassing nginx: `ws://127.0.0.1:8081`.
|
||||
|
||||
> **Client limitation:** `src/game_server/WebsocketGameClient.cpp` currently
|
||||
> speaks plain `ws://host:port` to `/` (no TLS, no path). Until TLS and path
|
||||
> support are added to the client, the graphical game can only connect directly
|
||||
> to a non-proxied server (e.g. `ws://127.0.0.1:8080`), not to the `wss://` URL.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- `Animation metadata failed ... Invalid animation file`: the `.anim` files are
|
||||
Git LFS pointer stubs (`version https://git-lfs...`). Fetch the real files with
|
||||
`git lfs pull` (or copy `resources/` from a full checkout) and rebuild.
|
||||
- `GameServer.LocationState` test currently fails on every platform: it expects
|
||||
`battle_zone` and `battle_zone_multiplayer` to share a start-state file, but
|
||||
`campaigns.json` points them to different files.
|
||||
|
||||
- `Server content setup failed`: the server was not started from the directory
|
||||
containing `resources/` (use `run-server.sh`).
|
||||
- `WebSocket listener setup failed`: port already in use (`ss -ltnp | grep 8081`).
|
||||
- `502 Bad Gateway` from nginx: service not running (`systemctl status
|
||||
shadow-gameserver`).
|
||||
- Connection drops after ~60 s: check `proxy_read_timeout` in the nginx location.
|
||||
- Compiler "Killed" / `internal compiler error`: out of memory, see the memory note.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Install to /etc/nginx/sites-available/api.shadowoverbishkek.com and symlink
|
||||
# into sites-enabled. Then: sudo certbot --nginx -d api.shadowoverbishkek.com
|
||||
server {
|
||||
server_name api.shadowoverbishkek.com;
|
||||
|
||||
access_log /var/log/nginx/api.shadowoverbishkek.com-access.log;
|
||||
error_log /var/log/nginx/api.shadowoverbishkek.com-error.log;
|
||||
|
||||
# wss://api.shadowoverbishkek.com/server001/ -> 127.0.0.1:8081
|
||||
location /server001/ {
|
||||
proxy_pass http://127.0.0.1:8081/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
|
||||
listen 443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/api.shadowoverbishkek.com/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/api.shadowoverbishkek.com/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
}
|
||||
|
||||
server {
|
||||
if ($host = api.shadowoverbishkek.com) {
|
||||
return 301 https://$host$request_uri;
|
||||
} # managed by Certbot
|
||||
|
||||
|
||||
server_name api.shadowoverbishkek.com;
|
||||
|
||||
listen 80;
|
||||
return 404; # managed by Certbot
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[Unit]
|
||||
Description=Shadow Over Bishkek GameServer (server001)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ubuntu
|
||||
ExecStart=/home/ubuntu/shadowoverbishkek-work/shadow-over-bishkek001/server/scripts/run-server.sh
|
||||
Environment=PORT=8081
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
MemoryMax=512M
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Configure and build the headless GameServer (Release).
|
||||
# Usage: server/scripts/build.sh (uses 1 job: safe for 2 GB RAM hosts)
|
||||
# JOBS=4 server/scripts/build.sh
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
cmake -S "$ROOT/server" -B "$ROOT/server/build" -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build "$ROOT/server/build" --target GameServer --parallel "${JOBS:-1}"
|
||||
echo "Built: $ROOT/server/build/GameServer"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start the multiplayer GameServer. Listens on loopback only; nginx proxies
|
||||
# wss://api.shadowoverbishkek.com/server001/ to it.
|
||||
# Overrides: CAMPAIGN, LOCATION, ADDRESS, PORT, BUILD_DIR
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
BUILD_DIR="${BUILD_DIR:-$ROOT/server/build}"
|
||||
CAMPAIGN="${CAMPAIGN:-battle_zone_multiplayer}"
|
||||
ADDRESS="${ADDRESS:-127.0.0.1}"
|
||||
PORT="${PORT:-8081}"
|
||||
|
||||
# Content paths are resolved from the working directory.
|
||||
cd "$BUILD_DIR"
|
||||
ARGS=(--campaign "$CAMPAIGN" --address "$ADDRESS" --port "$PORT")
|
||||
[ -n "${LOCATION:-}" ] && ARGS+=(--location "$LOCATION")
|
||||
exec ./GameServer "${ARGS[@]}"
|
||||
@@ -79,7 +79,7 @@ int main()
|
||||
return fail("Battle Zone modes do not share their initial state");
|
||||
}
|
||||
if (multiplayerBattleZoneConfig->networkPlayerSlots.size() != 2 ||
|
||||
multiplayerBattleZoneConfig->networkPlayerSlots[0] != "player" ||
|
||||
multiplayerBattleZoneConfig->networkPlayerSlots[0] != "player1" ||
|
||||
multiplayerBattleZoneConfig->networkPlayerSlots[1] !=
|
||||
"player2" ||
|
||||
multiplayerBattleZoneConfig->networkCompatibility.empty()) {
|
||||
@@ -140,7 +140,7 @@ int main()
|
||||
disabledPlayer->enabled = false;
|
||||
state.location->targetInteractiveObjectId = "object";
|
||||
state.location->targetInteractNpcId = "npc";
|
||||
FRG::PlayerStateMap disabledPlayers{{"player", disabledPlayer}};
|
||||
FRG::PlayerStateMap disabledPlayers{{"player1", disabledPlayer}};
|
||||
const FRG::LocationSimulationEvents disabledEvents =
|
||||
state.update(50, disabledPlayers, false);
|
||||
if (!disabledEvents.interactiveObjects.empty() ||
|
||||
@@ -165,7 +165,7 @@ int main()
|
||||
playerTwo->enabled = true;
|
||||
playerTwo->position = {2.f, 0.f, 0.f};
|
||||
FRG::PlayerStateMap combatPlayers{
|
||||
{"player", playerOne}, {"player2", playerTwo}};
|
||||
{"player1", playerOne}, {"player2", playerTwo}};
|
||||
|
||||
state.update(100, combatPlayers, false);
|
||||
if (hostileNpc.attackTargetId != "player2") {
|
||||
@@ -181,7 +181,7 @@ int main()
|
||||
|
||||
playerTwo->hp = 0.f;
|
||||
state.update(100, combatPlayers, false);
|
||||
if (hostileNpc.attackTargetId != "player") {
|
||||
if (hostileNpc.attackTargetId != "player1") {
|
||||
return fail("Hostile NPC did not acquire a replacement after target death");
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ int main()
|
||||
state.npcs["blocker"] = blocker;
|
||||
hostileNpc.position = Eigen::Vector3f::Zero();
|
||||
hostileNpc.homePosition = {5.f, 0.f, 0.f};
|
||||
hostileNpc.attackTargetId = "player";
|
||||
hostileNpc.attackTargetId = "player1";
|
||||
hostileNpc.requestedWalkTarget = playerOne->position;
|
||||
hostileNpc.walkTarget = playerOne->position;
|
||||
hostileNpc.pathWaypoints = {playerOne->position};
|
||||
@@ -267,22 +267,22 @@ int main()
|
||||
FRG::CampaignConfig multiplayerBattleZone = battleZone;
|
||||
multiplayerBattleZone.id = "battle_zone_multiplayer";
|
||||
multiplayerBattleZone.networkPlayerSlots = {
|
||||
"player", "player2"};
|
||||
"player1", "player2"};
|
||||
networkServer.startCampaign(multiplayerBattleZone, {}, true, false);
|
||||
const auto networkSnapshot = networkServer.snapshot();
|
||||
if (networkSnapshot->campaignId != "battle_zone_multiplayer")
|
||||
return fail("Network campaign ID was not retained");
|
||||
if (networkSnapshot->players.count("player") != 1)
|
||||
if (networkSnapshot->players.count("player1") != 1)
|
||||
return fail("Network campaign shared player was not created");
|
||||
if (networkSnapshot->players.count("player2") != 0)
|
||||
return fail("Network campaign unexpectedly created the test bot");
|
||||
if (networkSnapshot->players.at("player").state.enabled)
|
||||
if (networkSnapshot->players.at("player1").state.enabled)
|
||||
return fail("Network campaign player slot was enabled before connection");
|
||||
|
||||
nlohmann::json loadedNetworkState;
|
||||
networkServer.save(loadedNetworkState);
|
||||
loadedNetworkState["players"]["player2"] =
|
||||
loadedNetworkState["players"]["player"];
|
||||
loadedNetworkState["players"]["player1"];
|
||||
loadedNetworkState["players"]["player2"]["position"] =
|
||||
{4.f, 0.f, 5.f};
|
||||
networkServer.load(loadedNetworkState);
|
||||
@@ -308,7 +308,7 @@ int main()
|
||||
networkServer.submit(FRG::SetCharacterFacingAngleCommand{
|
||||
locationId, "player2", 1.5f, true});
|
||||
networkServer.submit(FRG::SetCharacterFaceTargetCommand{
|
||||
locationId, "player2", "player"});
|
||||
locationId, "player2", "player1"});
|
||||
networkServer.submit(FRG::SetCharacterEnabledCommand{
|
||||
locationId, "player2", false});
|
||||
const auto commandSnapshot = networkServer.snapshot();
|
||||
@@ -371,6 +371,7 @@ int main()
|
||||
moving->hp = 10.f;
|
||||
moving->pathWaypoints = {Eigen::Vector3f(8.f, 0.f, 0.f)};
|
||||
moving->movedLastTick = true;
|
||||
moving->attackTargetId = "stationary";
|
||||
FRG::PlayerStateMap occupancyPlayers{
|
||||
{"stationary", stationary}, {"moving", moving}};
|
||||
if (occupancyState.collectPathObstacles(
|
||||
@@ -378,7 +379,11 @@ int main()
|
||||
occupancyState.collectPathObstacles(
|
||||
"requester", occupancyPlayers, "moving").size() != 2 ||
|
||||
!occupancyState.collectPathObstacles(
|
||||
"stationary", occupancyPlayers).empty()) {
|
||||
"stationary", occupancyPlayers).empty() ||
|
||||
!occupancyState.collectPathObstacles(
|
||||
"moving", occupancyPlayers).empty() ||
|
||||
occupancyState.collectPathObstacles(
|
||||
"moving", occupancyPlayers, "stationary").size() != 1) {
|
||||
return fail("Stationary obstacle collection policy is incorrect");
|
||||
}
|
||||
|
||||
|
||||
@@ -23,12 +23,12 @@ int main()
|
||||
const nlohmann::json original{
|
||||
{"schemaVersion", 2},
|
||||
{"campaignId", "battle_zone_multiplayer"},
|
||||
{"players", {{"player", {{"hp", 200.0}}}}},
|
||||
{"locations", {{"uni_exterior", {{"state", savedLocationState}}}}}
|
||||
{"players", {{"player1", {{"hp", 200.0}}}}},
|
||||
{"locations", {{"park_korea", {{"state", savedLocationState}}}}}
|
||||
};
|
||||
FRG::InitialStateMessage initial;
|
||||
initial.state = original;
|
||||
initial.assignedPlayerId = "player";
|
||||
initial.assignedPlayerId = "player1";
|
||||
initial.snapshotTick = 42;
|
||||
initial.compatibilityFingerprint = "lockstep-v2:battle_zone_multiplayer";
|
||||
const std::string message = FRG::makeInitialStateMessage(initial);
|
||||
@@ -40,11 +40,11 @@ int main()
|
||||
require(parsed.state == original, "state payload did not round trip");
|
||||
require(parsed.snapshotTick == 42, "snapshot tick did not round trip");
|
||||
require(
|
||||
parsed.assignedPlayerId == "player",
|
||||
parsed.assignedPlayerId == "player1",
|
||||
"assigned player did not round trip");
|
||||
require(
|
||||
!parsed.state["locations"]["uni_exterior"]["state"].contains("cameraAzimuth") &&
|
||||
!parsed.state["locations"]["uni_exterior"]["state"].contains("cameraInclination"),
|
||||
!parsed.state["locations"]["park_korea"]["state"].contains("cameraAzimuth") &&
|
||||
!parsed.state["locations"]["park_korea"]["state"].contains("cameraInclination"),
|
||||
"local camera state leaked into the network payload");
|
||||
|
||||
FRG::DestinationInputMessage input;
|
||||
@@ -87,11 +87,11 @@ int main()
|
||||
FRG::TickMessage tick;
|
||||
tick.tickId = 43;
|
||||
tick.commands.push_back(FRG::ScheduledDestinationCommand{
|
||||
1, "player", parsedInput.target});
|
||||
1, "player1", parsedInput.target});
|
||||
tick.commands.push_back(FRG::ScheduledCharacterEnabledCommand{
|
||||
2, "uni_exterior", "player2", true});
|
||||
2, "park_korea", "player2", true});
|
||||
tick.commands.push_back(FRG::ScheduledCharacterActionCommand{
|
||||
3, "player", "set_target", "hostile_npc"});
|
||||
3, "player1", "set_target", "hostile_npc"});
|
||||
tick.stateHash = "0123456789abcdef";
|
||||
FRG::TickMessage parsedTick;
|
||||
require(
|
||||
@@ -147,9 +147,9 @@ int main()
|
||||
|
||||
FRG::LockstepTimeline timeline(2);
|
||||
const std::uint64_t firstSequence =
|
||||
timeline.schedule("player", Eigen::Vector3f(1.f, 0.f, 1.f));
|
||||
timeline.schedule("player1", Eigen::Vector3f(1.f, 0.f, 1.f));
|
||||
const std::uint64_t secondSequence =
|
||||
timeline.schedule("player_2", Eigen::Vector3f(2.f, 0.f, 2.f));
|
||||
timeline.schedule("player2", Eigen::Vector3f(2.f, 0.f, 2.f));
|
||||
const FRG::TickMessage emptyTick = timeline.advance();
|
||||
require(
|
||||
emptyTick.tickId == 1 && emptyTick.commands.empty(),
|
||||
@@ -163,8 +163,8 @@ int main()
|
||||
commandTick.commands[1]).commandSequence == secondSequence,
|
||||
"scheduled commands were not finalized in stable order");
|
||||
|
||||
timeline.scheduleCharacterEnabled("uni_exterior", "player2", true);
|
||||
timeline.scheduleCharacterAction("player", "clear_attack_target");
|
||||
timeline.scheduleCharacterEnabled("park_korea", "player2", true);
|
||||
timeline.scheduleCharacterAction("player1", "clear_attack_target");
|
||||
const FRG::TickMessage presenceTick = timeline.advance();
|
||||
require(
|
||||
presenceTick.commands.size() == 1 &&
|
||||
|
||||
@@ -59,13 +59,13 @@ int main()
|
||||
|
||||
FRG::CampaignConfig campaign;
|
||||
campaign.id = "test_lockstep";
|
||||
campaign.networkPlayerSlots = {"player", "player2"};
|
||||
campaign.networkPlayerSlots = {"player1", "player2"};
|
||||
|
||||
FRG::InternalGameServer authoritative;
|
||||
authoritative.startCampaign(campaign, {}, true, false);
|
||||
nlohmann::json initialState;
|
||||
authoritative.save(initialState);
|
||||
initialState["players"]["player2"] = initialState["players"]["player"];
|
||||
initialState["players"]["player2"] = initialState["players"]["player1"];
|
||||
FRG::LocationGameState testLocation;
|
||||
auto hostileNpc = std::make_shared<FRG::CharacterState>();
|
||||
hostileNpc->enabled = true;
|
||||
@@ -77,7 +77,7 @@ int main()
|
||||
initialState["locations"]["test_location"] = std::move(testLocationState);
|
||||
authoritative.load(initialState);
|
||||
FRG::WebsocketGameServer server(
|
||||
authoritative, {"player", "player2"}, "lockstep-v2:test_lockstep",
|
||||
authoritative, {"player1", "player2"}, "lockstep-v2:test_lockstep",
|
||||
"127.0.0.1", 0);
|
||||
auto client = std::make_unique<FRG::WebsocketGameClient>(
|
||||
"127.0.0.1", std::to_string(server.port()), "/");
|
||||
@@ -91,7 +91,7 @@ int main()
|
||||
return client->initialized();
|
||||
}),
|
||||
"client did not receive initial state");
|
||||
require(client->localPlayerId() == "player", "wrong player slot assigned");
|
||||
require(client->localPlayerId() == "player1", "wrong player slot assigned");
|
||||
|
||||
auto secondClient = std::make_unique<FRG::WebsocketGameClient>(
|
||||
"127.0.0.1", std::to_string(server.port()), "/");
|
||||
@@ -120,9 +120,9 @@ int main()
|
||||
client->update(0);
|
||||
secondClient->update(0);
|
||||
return client->snapshot()->players.at("player2").state.enabled &&
|
||||
secondClient->snapshot()->players.at("player").state.enabled;
|
||||
secondClient->snapshot()->players.at("player1").state.enabled;
|
||||
}) &&
|
||||
authoritative.snapshot()->players.at("player").state.enabled &&
|
||||
authoritative.snapshot()->players.at("player1").state.enabled &&
|
||||
authoritative.snapshot()->players.at("player2").state.enabled,
|
||||
"connection presence was not broadcast to all replicas");
|
||||
|
||||
@@ -140,16 +140,16 @@ int main()
|
||||
const auto first = client->snapshot();
|
||||
const auto second = secondClient->snapshot();
|
||||
const auto host = authoritative.snapshot();
|
||||
return first->players.at("player").state.requestedWalkTarget.isApprox(
|
||||
return first->players.at("player1").state.requestedWalkTarget.isApprox(
|
||||
Eigen::Vector3f(1.23f, 0.f, 2.35f)) &&
|
||||
second->players.at("player2").state.requestedWalkTarget.isApprox(
|
||||
Eigen::Vector3f(-1.23f, 0.f, 3.46f)) &&
|
||||
first->players.at("player2").state.requestedWalkTarget.isApprox(
|
||||
second->players.at("player2").state.requestedWalkTarget) &&
|
||||
second->players.at("player").state.requestedWalkTarget.isApprox(
|
||||
first->players.at("player").state.requestedWalkTarget) &&
|
||||
host->players.at("player").state.requestedWalkTarget.isApprox(
|
||||
first->players.at("player").state.requestedWalkTarget) &&
|
||||
second->players.at("player1").state.requestedWalkTarget.isApprox(
|
||||
first->players.at("player1").state.requestedWalkTarget) &&
|
||||
host->players.at("player1").state.requestedWalkTarget.isApprox(
|
||||
first->players.at("player1").state.requestedWalkTarget) &&
|
||||
host->players.at("player2").state.requestedWalkTarget.isApprox(
|
||||
second->players.at("player2").state.requestedWalkTarget);
|
||||
});
|
||||
@@ -160,9 +160,9 @@ int main()
|
||||
std::cerr << "destination revisions " << first->revision << ", "
|
||||
<< second->revision << ", " << host->revision << "\n"
|
||||
<< "player targets "
|
||||
<< first->players.at("player").state.requestedWalkTarget.transpose()
|
||||
<< first->players.at("player1").state.requestedWalkTarget.transpose()
|
||||
<< " / "
|
||||
<< host->players.at("player").state.requestedWalkTarget.transpose()
|
||||
<< host->players.at("player1").state.requestedWalkTarget.transpose()
|
||||
<< "\nplayer2 targets "
|
||||
<< second->players.at("player2").state.requestedWalkTarget.transpose()
|
||||
<< " / "
|
||||
@@ -183,11 +183,11 @@ int main()
|
||||
waitFor([&]() {
|
||||
client->update(0);
|
||||
secondClient->update(0);
|
||||
return client->snapshot()->players.at("player").state.attackTargetId ==
|
||||
return client->snapshot()->players.at("player1").state.attackTargetId ==
|
||||
"hostile_npc" &&
|
||||
secondClient->snapshot()->players.at("player").state.attackTargetId ==
|
||||
secondClient->snapshot()->players.at("player1").state.attackTargetId ==
|
||||
"hostile_npc" &&
|
||||
authoritative.snapshot()->players.at("player").state.attackTargetId ==
|
||||
authoritative.snapshot()->players.at("player1").state.attackTargetId ==
|
||||
"hostile_npc";
|
||||
}),
|
||||
"target command was not delayed and rebroadcast");
|
||||
@@ -202,9 +202,9 @@ int main()
|
||||
waitFor([&]() {
|
||||
client->update(0);
|
||||
secondClient->update(0);
|
||||
return client->snapshot()->players.at("player").state.attackTargetId.empty() &&
|
||||
secondClient->snapshot()->players.at("player").state.attackTargetId.empty() &&
|
||||
authoritative.snapshot()->players.at("player").state.attackTargetId.empty();
|
||||
return client->snapshot()->players.at("player1").state.attackTargetId.empty() &&
|
||||
secondClient->snapshot()->players.at("player1").state.attackTargetId.empty() &&
|
||||
authoritative.snapshot()->players.at("player1").state.attackTargetId.empty();
|
||||
}),
|
||||
"clear attack target command was not delayed and rebroadcast");
|
||||
|
||||
|
||||
+7
-1
@@ -28,6 +28,12 @@ namespace FRG {
|
||||
return Eigen::Vector3f(j[0].get<float>(), j[1].get<float>(), j[2].get<float>());
|
||||
}
|
||||
|
||||
|
||||
static Vec3fp readVec3fp(const json& j) {
|
||||
if (!j.is_array() || j.size() != 3) return Vec3fp();
|
||||
return Vec3fp(static_cast<fp>(j[0].get<double>()), static_cast<fp>(j[1].get<double>()), static_cast<fp>(j[2].get<double>()));
|
||||
}
|
||||
|
||||
bool LocationConfig::fromFile(const std::string& path, const std::string& zipFile, LocationConfig& out) {
|
||||
json j;
|
||||
if (!parseJsonFile(path, zipFile, j)) return false;
|
||||
@@ -43,7 +49,7 @@ namespace FRG {
|
||||
out.teleportsJsonPath = j.value("teleports", "");
|
||||
out.triggerZonesJsonPath = j.value("triggerZones", "");
|
||||
out.lightsJsonPath = j.value("lights", "");
|
||||
if (j.contains("playerPosition")) out.playerPosition = readVec3(j["playerPosition"]);
|
||||
if (j.contains("playerPosition")) out.playerPosition = readVec3fp(j["playerPosition"]);
|
||||
out.lockInteractiveObjectsDuringTutorial = j.value("lockInteractiveObjectsDuringTutorial", false);
|
||||
out.loadingScreenPath = j.value("loadingScreen", "");
|
||||
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "navigation/PathFinder.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
@@ -21,7 +22,7 @@ namespace FRG {
|
||||
std::string teleportsJsonPath;
|
||||
std::string triggerZonesJsonPath;
|
||||
std::string lightsJsonPath;
|
||||
Eigen::Vector3f playerPosition = Eigen::Vector3f::Zero();
|
||||
Vec3fp playerPosition;
|
||||
|
||||
// Where the taxi drives in when the player calls it. No taxi when absent.
|
||||
struct Taxi {
|
||||
|
||||
+33
-43
@@ -42,19 +42,9 @@ bool Character::isMoving() const {
|
||||
return state && state->isMoving();
|
||||
}
|
||||
|
||||
Eigen::Vector3f Character::getRequestedWalkTarget() const {
|
||||
auto state = stateSnapshot();
|
||||
return state ? state->getRequestedWalkTarget() : Eigen::Vector3f::Zero();
|
||||
}
|
||||
|
||||
Eigen::Vector3f Character::getCurrentNavigationTarget() const {
|
||||
auto state = stateSnapshot();
|
||||
return state ? state->getCurrentNavigationTarget() : Eigen::Vector3f::Zero();
|
||||
}
|
||||
|
||||
Eigen::Vector3f Character::renderPosition() const {
|
||||
auto state = stateSnapshot();
|
||||
return state ? interpolatedState(*state).position : Eigen::Vector3f::Zero();
|
||||
return state ? interpolatedState(*state).position.toEigen() : Eigen::Vector3f::Zero();
|
||||
}
|
||||
|
||||
float Character::getHp() const {
|
||||
@@ -181,8 +171,8 @@ CharacterState Character::interpolatedState(const CharacterState& state) const {
|
||||
const float alpha = (std::max)(0.0f, (std::min)(1.0f, elapsedMs / kSimulationTickMs));
|
||||
|
||||
CharacterState result = state;
|
||||
result.position = previousPresentationSample_.position +
|
||||
alpha * (currentPresentationSample_.position - previousPresentationSample_.position);
|
||||
result.position = Vec3fp::fromEigen(previousPresentationSample_.position +
|
||||
alpha * (currentPresentationSample_.position - previousPresentationSample_.position));
|
||||
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
constexpr float kTwoPi = 2.0f * kPi;
|
||||
@@ -190,7 +180,7 @@ CharacterState Character::interpolatedState(const CharacterState& state) const {
|
||||
previousPresentationSample_.facingAngle;
|
||||
while (angleDelta > kPi) angleDelta -= kTwoPi;
|
||||
while (angleDelta < -kPi) angleDelta += kTwoPi;
|
||||
result.facingAngle = previousPresentationSample_.facingAngle + alpha * angleDelta;
|
||||
result.facingAngle = static_cast<fp>(previousPresentationSample_.facingAngle + alpha * angleDelta);
|
||||
if (previousPresentationSample_.currentState == state.currentState &&
|
||||
currentPresentationSample_.currentState == state.currentState) {
|
||||
float previousFrame = previousPresentationSample_.animationFrame;
|
||||
@@ -235,8 +225,8 @@ void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
PresentationSample sample;
|
||||
sample.position = state.position;
|
||||
sample.facingAngle = state.facingAngle;
|
||||
sample.position = state.position.toEigen();
|
||||
sample.facingAngle = static_cast<float>(state.facingAngle);
|
||||
sample.animationFrame = state.animationFrame;
|
||||
sample.currentState = state.currentState;
|
||||
sample.enabled = state.enabled;
|
||||
@@ -299,8 +289,8 @@ void Character::draw(Renderer& renderer) {
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
@@ -353,8 +343,8 @@ void Character::drawGpuSkinning(Renderer& renderer, const CharacterState& state)
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
@@ -417,8 +407,8 @@ void Character::drawAttachedWeapon(Renderer& renderer, const CharacterState& sta
|
||||
Eigen::Matrix4f currentBone = anim.gpuSkinningShaderData.skinningMatrices[boneIdx] * bindBone;
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix(Eigen::Vector3f(currentBone.block<3, 1>(0, 3)));
|
||||
@@ -452,8 +442,8 @@ void Character::drawShadowDepthCpu(Renderer& renderer, const CharacterState& sta
|
||||
if (it == animations.end()) return;
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
@@ -488,8 +478,8 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterSt
|
||||
renderer.shaderManager.PushShader(shadowSkinningShader);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
@@ -561,8 +551,8 @@ void Character::drawCpuWithShadow(Renderer& renderer, const CharacterState& stat
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
@@ -623,8 +613,8 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const CharacterSta
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
@@ -726,8 +716,8 @@ void Character::drawNightGpuSkinning(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
@@ -798,8 +788,8 @@ void Character::drawNightCpu(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
@@ -900,8 +890,8 @@ void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
@@ -985,8 +975,8 @@ void Character::drawNightCpuWithShadow(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.TranslateMatrix({ static_cast<float>(state.position.x), static_cast<float>(state.position.y), static_cast<float>(state.position.z) });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-static_cast<float>(state.facingAngle), Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
@@ -1027,7 +1017,7 @@ void Character::setupHitSparks(std::shared_ptr<Texture> sparkTexture) {
|
||||
}
|
||||
|
||||
void Character::presentDamage(float hp, float maxHp,
|
||||
const Eigen::Vector3f& attackDirection,
|
||||
const Vec3fp& attackDirection,
|
||||
const std::string& attackerId) {
|
||||
if (onHpChanged)
|
||||
onHpChanged(hp, maxHp, attackerId);
|
||||
@@ -1037,9 +1027,9 @@ void Character::presentDamage(float hp, float maxHp,
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
|
||||
Eigen::Vector3f emitPos = state.position + Eigen::Vector3f(0.f, 1.f, 0.f);
|
||||
Eigen::Vector3f emitPos = state.position.toEigen() + Eigen::Vector3f(0.f, 1.f, 0.f);
|
||||
hitSparkEmitter.resetEmissionPoints({ emitPos });
|
||||
hitSparkEmitter.setEmissionDirection(attackDirection);
|
||||
hitSparkEmitter.setEmissionDirection(attackDirection.toEigen());
|
||||
|
||||
static std::mt19937 gen(std::random_device{}());
|
||||
std::uniform_int_distribution<int> countDist(10, 15);
|
||||
@@ -1065,7 +1055,7 @@ static void drawCharacterNameLabel(
|
||||
if (!state.enabled || state.hp <= 0.f || displayName.empty()) return;
|
||||
|
||||
Eigen::Vector4f worldPos(
|
||||
state.position.x(), state.position.y() + 1.83f, state.position.z(), 1.f);
|
||||
static_cast<float>(state.position.x), static_cast<float>(state.position.y) + 1.83f, static_cast<float>(state.position.z), 1.f);
|
||||
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
|
||||
if (clip.w() <= 0.f) return;
|
||||
|
||||
@@ -1126,7 +1116,7 @@ void Character::drawHealthBar(Renderer& renderer,
|
||||
if (state.hp <= 0.f) return;
|
||||
if (state.initialHp <= 0.f) return;
|
||||
|
||||
Eigen::Vector4f worldPos(state.position.x(), state.position.y() + 1.75f, state.position.z(), 1.f);
|
||||
Eigen::Vector4f worldPos(static_cast<float>(state.position.x), static_cast<float>(state.position.y) + 1.75f, static_cast<float>(state.position.z), 1.f);
|
||||
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
|
||||
if (clip.w() <= 0.f) return;
|
||||
float ndcX = clip.x() / clip.w();
|
||||
|
||||
+1
-3
@@ -55,7 +55,7 @@ public:
|
||||
void update(int64_t deltaMs, const CharacterUpdateEvents* events = nullptr);
|
||||
|
||||
// ---- Damage ----
|
||||
void presentDamage(float hp, float maxHp, const Eigen::Vector3f& attackDirection,
|
||||
void presentDamage(float hp, float maxHp, const Vec3fp& attackDirection,
|
||||
const std::string& attackerId);
|
||||
|
||||
// ---- Spark setup ----
|
||||
@@ -93,8 +93,6 @@ public:
|
||||
|
||||
// ---- Query helpers (read immutable snapshots) ----
|
||||
bool isMoving() const;
|
||||
Eigen::Vector3f getRequestedWalkTarget() const;
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||
Eigen::Vector3f renderPosition() const;
|
||||
float getHp() const;
|
||||
|
||||
|
||||
+220
-189
@@ -9,7 +9,13 @@ namespace {
|
||||
// Tick counts are authoritative and therefore independent of render rate.
|
||||
constexpr float ATTACK_COOLDOWN_TIME = 1.6f;
|
||||
constexpr float HOME_DRIFT_CHECK_INTERVAL = 5.0f;
|
||||
fp HOME_DRIFT_CHECK_INTERVAL_FP = static_cast<fp>(HOME_DRIFT_CHECK_INTERVAL);
|
||||
constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
|
||||
|
||||
fp TARGET_REPLAN_THRESHOLD_SQUARED = static_cast<fp>(TARGET_REPLAN_THRESHOLD * TARGET_REPLAN_THRESHOLD);
|
||||
|
||||
|
||||
|
||||
constexpr float PI = 3.14159265358979323846f;
|
||||
constexpr int BLOCKED_REPLAN_TICKS = 3;
|
||||
|
||||
@@ -25,8 +31,8 @@ std::uint32_t stableIdHash(const std::string& value)
|
||||
}
|
||||
|
||||
void CharacterState::configureAsPlayer() {
|
||||
walkSpeed = 3.0f;
|
||||
rotationSpeed = 8.0f;
|
||||
walkSpeed = static_cast<fp>(3.0f);
|
||||
rotationSpeed = static_cast < fp>(8.0f);
|
||||
modelScale = 1.f;
|
||||
modelCorrectionRotation = Eigen::Quaternionf(
|
||||
Eigen::AngleAxisf(PI, Eigen::Vector3f::UnitY()) *
|
||||
@@ -66,12 +72,12 @@ void CharacterState::scheduleNextNavigationRetry()
|
||||
}
|
||||
|
||||
bool CharacterState::isMoving() const {
|
||||
Eigen::Vector3f toTarget = walkTarget - position;
|
||||
toTarget.y() = 0.f;
|
||||
return !pathWaypoints.empty() || toTarget.norm() > kWalkThreshold;
|
||||
Vec3fp toTarget = walkTarget - position;
|
||||
toTarget.y = static_cast<fp>(0.f);
|
||||
return !pathWaypoints.empty() || toTarget.normSquared() > kWalkThreshold_SQUARED();
|
||||
}
|
||||
|
||||
Eigen::Vector3f CharacterState::getCurrentNavigationTarget() const {
|
||||
Vec3fp CharacterState::getCurrentNavigationTarget() const {
|
||||
if (!pathWaypoints.empty() && currentWaypointIndex < pathWaypoints.size()) {
|
||||
return pathWaypoints[currentWaypointIndex];
|
||||
}
|
||||
@@ -79,32 +85,34 @@ Eigen::Vector3f CharacterState::getCurrentNavigationTarget() const {
|
||||
}
|
||||
|
||||
void CharacterState::stopInPlace() {
|
||||
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||
walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
|
||||
requestedWalkTarget = walkTarget;
|
||||
pathWaypoints.clear();
|
||||
currentWaypointIndex = 0;
|
||||
onArrivedCallbackName.clear();
|
||||
movementBlockedTicks = 0;
|
||||
navigationRetryTicks = 0;
|
||||
blockingCharacterId.clear();
|
||||
velocity = Vec2fp(fp(0), fp(0));
|
||||
preferredVelocity = Vec2fp(fp(0), fp(0));
|
||||
}
|
||||
|
||||
void CharacterState::setTarget(
|
||||
const Eigen::Vector3f& target,
|
||||
const Vec3fp& target,
|
||||
std::string callbackName,
|
||||
const LocationNetworkState& location,
|
||||
const std::string& entityId,
|
||||
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles)
|
||||
const std::string& entityId)
|
||||
{
|
||||
const Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
|
||||
|
||||
|
||||
const Vec3fp normalizedTarget(target.x, static_cast<fp>(0.f), target.z);
|
||||
const bool sameRequestedTarget =
|
||||
(normalizedTarget - requestedWalkTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
(normalizedTarget - requestedWalkTarget).normSquared() <= TARGET_REPLAN_THRESHOLD_SQUARED;
|
||||
const bool alreadyMovingToTarget =
|
||||
!pathWaypoints.empty() ||
|
||||
(walkTarget - normalizedTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
(walkTarget - normalizedTarget).normSquared() <= TARGET_REPLAN_THRESHOLD_SQUARED;
|
||||
const bool stoppedAfterFailedPath =
|
||||
pathWaypoints.empty() &&
|
||||
(walkTarget - position).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
(walkTarget - position).normSquared() <= TARGET_REPLAN_THRESHOLD_SQUARED;
|
||||
if (callbackName.empty() && sameRequestedTarget &&
|
||||
(alreadyMovingToTarget || stoppedAfterFailedPath)) {
|
||||
return;
|
||||
@@ -114,44 +122,62 @@ void CharacterState::setTarget(
|
||||
onArrivedCallbackName = std::move(callbackName);
|
||||
movementBlockedTicks = 0;
|
||||
navigationRetryTicks = 0;
|
||||
blockingCharacterId.clear();
|
||||
pathWaypoints = location.navigationMaps.empty()
|
||||
? std::vector<Eigen::Vector3f>{}
|
||||
: location.planPathExceptMyself(
|
||||
position, normalizedTarget, entityId, dynamicObstacles);
|
||||
currentWaypointIndex = 0;
|
||||
|
||||
// В setTarget и forceReplan:
|
||||
if (location.activeNavigationIndex < 0 ||
|
||||
location.activeNavigationIndex >= static_cast<int>(location.navigationMaps.size())) {
|
||||
walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
|
||||
return;
|
||||
}
|
||||
|
||||
pathWaypoints = location.navigationMaps[location.activeNavigationIndex]
|
||||
.findPathToNearest(this->position, normalizedTarget);
|
||||
|
||||
// Сразу целимся во второй элемент (индекс 1), так как индекс 0 — это точка старта
|
||||
currentWaypointIndex = (pathWaypoints.size() > 1) ? 1 : 0;
|
||||
|
||||
if (!pathWaypoints.empty()) {
|
||||
for (Eigen::Vector3f& waypoint : pathWaypoints) waypoint.y() = 0.f;
|
||||
for (Vec3fp& waypoint : pathWaypoints)
|
||||
{
|
||||
waypoint.y = static_cast<fp>(0.f);
|
||||
}
|
||||
walkTarget = pathWaypoints.back();
|
||||
return;
|
||||
}
|
||||
|
||||
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||
walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
|
||||
if (!isPlayer) scheduleNextNavigationRetry();
|
||||
}
|
||||
|
||||
void CharacterState::forceReplan(
|
||||
const LocationNetworkState& location,
|
||||
const std::string& entityId,
|
||||
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles)
|
||||
const std::string& entityId)
|
||||
{
|
||||
const Eigen::Vector3f normalizedTarget(
|
||||
requestedWalkTarget.x(), 0.f, requestedWalkTarget.z());
|
||||
pathWaypoints = location.navigationMaps.empty()
|
||||
? std::vector<Eigen::Vector3f>{}
|
||||
: location.planPathExceptMyself(
|
||||
position, normalizedTarget, entityId, dynamicObstacles);
|
||||
currentWaypointIndex = 0;
|
||||
movementBlockedTicks = 0;
|
||||
navigationRetryTicks = 0;
|
||||
if (!pathWaypoints.empty()) {
|
||||
for (Eigen::Vector3f& waypoint : pathWaypoints) waypoint.y() = 0.f;
|
||||
walkTarget = pathWaypoints.back();
|
||||
blockingCharacterId.clear();
|
||||
const Vec3fp normalizedTarget(
|
||||
requestedWalkTarget.x, static_cast<fp>(0.f), requestedWalkTarget.z);
|
||||
|
||||
if (location.activeNavigationIndex < 0 ||
|
||||
location.activeNavigationIndex >= static_cast<int>(location.navigationMaps.size())) {
|
||||
walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
|
||||
return;
|
||||
}
|
||||
|
||||
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||
pathWaypoints = location.navigationMaps[location.activeNavigationIndex]
|
||||
.findPathToNearest(this->position, normalizedTarget);
|
||||
|
||||
currentWaypointIndex = (pathWaypoints.size() > 1) ? 1 : 0;
|
||||
movementBlockedTicks = 0;
|
||||
navigationRetryTicks = 0;
|
||||
|
||||
if (!pathWaypoints.empty()) {
|
||||
for (Vec3fp& waypoint : pathWaypoints) {
|
||||
waypoint.y = static_cast<fp>(0.f);
|
||||
}
|
||||
walkTarget = pathWaypoints.back();
|
||||
return;
|
||||
}
|
||||
|
||||
walkTarget = Vec3fp(position.x, static_cast<fp>(0.f), position.z);
|
||||
if (!isPlayer) scheduleNextNavigationRetry();
|
||||
}
|
||||
|
||||
@@ -213,145 +239,134 @@ CharacterUpdateEvents CharacterState::update(
|
||||
attack = 0;
|
||||
attackTargetId.clear();
|
||||
} else {
|
||||
const float distance = (attackTarget->position - position).norm();
|
||||
if (distance >= visibleRange) {
|
||||
if ((attackTarget->position - requestedWalkTarget).norm() >
|
||||
TARGET_REPLAN_THRESHOLD) {
|
||||
const fp distanceSquared = (attackTarget->position - position).normSquared();
|
||||
if (distanceSquared >= visibleRange* visibleRange) {
|
||||
if ((attackTarget->position - requestedWalkTarget).normSquared() >
|
||||
TARGET_REPLAN_THRESHOLD_SQUARED) {
|
||||
events.navigationRequested = true;
|
||||
events.navigationTarget = attackTarget->position;
|
||||
}
|
||||
battle_state = 0;
|
||||
} else if (distance >= 1.25f) {
|
||||
if ((attackTarget->position - requestedWalkTarget).norm() > TARGET_REPLAN_THRESHOLD) {
|
||||
} else if (distanceSquared >= static_cast<fp>(1.25f*1.25f)) {
|
||||
if ((attackTarget->position - requestedWalkTarget).normSquared() > TARGET_REPLAN_THRESHOLD_SQUARED) {
|
||||
events.navigationRequested = true;
|
||||
events.navigationTarget = attackTarget->position;
|
||||
}
|
||||
battle_state = 0;
|
||||
} else {
|
||||
battle_state = 1;
|
||||
velocity = Vec2fp(fp(0), fp(0));
|
||||
}
|
||||
}
|
||||
|
||||
const Eigen::Vector3f activeTarget = getCurrentNavigationTarget();
|
||||
const Eigen::Vector3f lookTarget =
|
||||
battle_state == 1 && attackTarget ? attackTarget->position : activeTarget;
|
||||
const Vec3fp activeTarget = getCurrentNavigationTarget();
|
||||
|
||||
if (currentState == AnimationState::WALK || currentState == AnimationState::STAND) {
|
||||
Eigen::Vector3f toTarget = activeTarget - position;
|
||||
toTarget.y() = 0.f;
|
||||
const float distance = toTarget.norm();
|
||||
if (distance > kWalkThreshold) {
|
||||
const Eigen::Vector3f direction = toTarget / distance;
|
||||
const float moveAmount = walkSpeed * static_cast<float>(deltaMs) / 1000.f;
|
||||
Eigen::Vector3f candidate =
|
||||
moveAmount >= distance ? activeTarget : position + direction * moveAmount;
|
||||
candidate.y() = 0.f;
|
||||
const CharacterMovementObstacle* blocker = nullptr;
|
||||
if (context.movementObstacles) {
|
||||
for (const CharacterMovementObstacle& obstacle :
|
||||
*context.movementObstacles) {
|
||||
if (obstacle.id == context.characterId) continue;
|
||||
Eigen::Vector3f startDelta = position - obstacle.position;
|
||||
Eigen::Vector3f endDelta = candidate - obstacle.position;
|
||||
startDelta.y() = 0.f;
|
||||
endDelta.y() = 0.f;
|
||||
const float minimumDistance =
|
||||
movementContactRadius + obstacle.radius;
|
||||
const float minimumDistanceSq =
|
||||
minimumDistance * minimumDistance;
|
||||
const Eigen::Vector3f segment = candidate - position;
|
||||
const float segmentLengthSq =
|
||||
segment.x() * segment.x() + segment.z() * segment.z();
|
||||
float t = 0.f;
|
||||
if (segmentLengthSq > 1e-8f) {
|
||||
t = -(startDelta.x() * segment.x() +
|
||||
startDelta.z() * segment.z()) / segmentLengthSq;
|
||||
t = (std::max)(0.f, (std::min)(1.f, t));
|
||||
}
|
||||
Eigen::Vector3f closest = startDelta + segment * t;
|
||||
closest.y() = 0.f;
|
||||
const bool movingCloser =
|
||||
endDelta.squaredNorm() + 1e-6f < startDelta.squaredNorm();
|
||||
if (movingCloser &&
|
||||
closest.squaredNorm() < minimumDistanceSq) {
|
||||
blocker = &obstacle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (blocker) {
|
||||
blockingCharacterId = blocker->id;
|
||||
currentState = AnimationState::STAND;
|
||||
const bool sameDirection =
|
||||
blocker->moving &&
|
||||
blocker->movementDirection.dot(direction) > 0.5f;
|
||||
if (sameDirection) {
|
||||
movementBlockedTicks = 0;
|
||||
} else if (++movementBlockedTicks >= BLOCKED_REPLAN_TICKS) {
|
||||
if (isPlayer) {
|
||||
stopInPlace();
|
||||
} else {
|
||||
pathWaypoints.clear();
|
||||
currentWaypointIndex = 0;
|
||||
walkTarget =
|
||||
Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||
movementBlockedTicks = 0;
|
||||
scheduleNextNavigationRetry();
|
||||
navigationPausedThisTick = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
movedLastTick =
|
||||
(candidate - position).squaredNorm() > 1e-8f;
|
||||
position = candidate;
|
||||
movementBlockedTicks = 0;
|
||||
blockingCharacterId.clear();
|
||||
}
|
||||
targetFacingAngle = std::atan2(direction.x(), -direction.z());
|
||||
if (!blocker) currentState = AnimationState::WALK;
|
||||
} else {
|
||||
currentState = AnimationState::STAND;
|
||||
const bool completedPath = !pathWaypoints.empty();
|
||||
const bool hasNextWaypoint =
|
||||
!pathWaypoints.empty() && currentWaypointIndex + 1 < pathWaypoints.size();
|
||||
if (hasNextWaypoint) {
|
||||
preferredVelocity = Vec2fp(fp(0), fp(0));
|
||||
|
||||
if (hp > 0.f && battle_state == 0 && !pathWaypoints.empty() &&
|
||||
(currentState == AnimationState::WALK || currentState == AnimationState::STAND))
|
||||
{
|
||||
// Если мы уже достигли промежуточного waypoint'а, сразу переключаемся на следующий в этом же тике
|
||||
while (!pathWaypoints.empty() && currentWaypointIndex + 1 < pathWaypoints.size()) {
|
||||
Vec3fp diff = pathWaypoints[currentWaypointIndex] - position;
|
||||
diff.y = fp(0);
|
||||
if (diff.normSquared() <= kWalkThreshold_SQUARED()) {
|
||||
++currentWaypointIndex;
|
||||
} else {
|
||||
pathWaypoints.clear();
|
||||
currentWaypointIndex = 0;
|
||||
if (completedPath &&
|
||||
(requestedWalkTarget - position).norm() >
|
||||
TARGET_REPLAN_THRESHOLD &&
|
||||
!isPlayer)
|
||||
scheduleNextNavigationRetry();
|
||||
if (completedPath && !onArrivedCallbackName.empty()) {
|
||||
events.arrivedCallbackName = onArrivedCallbackName;
|
||||
onArrivedCallbackName.clear();
|
||||
}
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const Vec3fp currentTarget = getCurrentNavigationTarget();
|
||||
Vec3fp toTarget = currentTarget - position;
|
||||
toTarget.y = fp(0);
|
||||
const fp distanceSquared = toTarget.normSquared();
|
||||
|
||||
if (distanceSquared > kWalkThreshold_SQUARED()) {
|
||||
const fp distance = fpm::sqrt(distanceSquared);
|
||||
const fp dtSec = static_cast<fp>(deltaMs) / fp(1000);
|
||||
|
||||
fp desiredSpeed = distance / dtSec;
|
||||
if (desiredSpeed > walkSpeed) {
|
||||
desiredSpeed = walkSpeed;
|
||||
}
|
||||
|
||||
preferredVelocity = Vec2fp(
|
||||
(toTarget.x / distance) * desiredSpeed,
|
||||
(toTarget.z / distance) * desiredSpeed
|
||||
);
|
||||
|
||||
// Если стартуем с места — сразу целим поворот на точку,
|
||||
// а в движении угол скорректирует Фаза 3 по реальному вектору ORCA (newVel)
|
||||
if (velocity.lengthSq() <= static_cast<fp>(0.05f)) {
|
||||
targetFacingAngle = fpm::atan2(toTarget.x, -toTarget.z);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Дошли до ФИНАЛЬНОЙ точки маршрута
|
||||
velocity = Vec2fp(fp(0), fp(0));
|
||||
if (currentState == AnimationState::WALK) {
|
||||
currentState = AnimationState::STAND;
|
||||
resetAnim = true;
|
||||
}
|
||||
const bool completedPath = !pathWaypoints.empty();
|
||||
pathWaypoints.clear();
|
||||
currentWaypointIndex = 0;
|
||||
|
||||
if (completedPath &&
|
||||
(requestedWalkTarget - position).normSquared() > TARGET_REPLAN_THRESHOLD_SQUARED &&
|
||||
!isPlayer) {
|
||||
scheduleNextNavigationRetry();
|
||||
}
|
||||
if (completedPath && !onArrivedCallbackName.empty()) {
|
||||
events.arrivedCallbackName = onArrivedCallbackName;
|
||||
onArrivedCallbackName.clear();
|
||||
}
|
||||
if (faceTarget) {
|
||||
Eigen::Vector3f toFace = faceTarget->position - position;
|
||||
toFace.y() = 0.f;
|
||||
const float distanceToFace = toFace.norm();
|
||||
targetFacingAngle = distanceToFace > 1e-3f
|
||||
? std::atan2(toFace.x() / distanceToFace, -toFace.z() / distanceToFace)
|
||||
Vec3fp toFace = faceTarget->position - position;
|
||||
toFace.y = fp(0);
|
||||
const fp distanceToFace = fpm::sqrt(toFace.normSquared());
|
||||
targetFacingAngle = distanceToFace > static_cast<fp>(1e-3f)
|
||||
? fpm::atan2(toFace.x / distanceToFace, -toFace.z / distanceToFace)
|
||||
: facingAngle;
|
||||
} else if (!faceTargetId.empty()) {
|
||||
}
|
||||
else if (!faceTargetId.empty()) {
|
||||
faceTargetId.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (hp > 0.f && battle_state == 0 && pathWaypoints.empty() &&
|
||||
(currentState == AnimationState::WALK || currentState == AnimationState::STAND))
|
||||
{
|
||||
// Маршрута нет — стоим на месте и при необходимости поворачиваемся к faceTarget
|
||||
velocity = Vec2fp(fp(0), fp(0));
|
||||
if (currentState == AnimationState::WALK) {
|
||||
currentState = AnimationState::STAND;
|
||||
resetAnim = true;
|
||||
}
|
||||
if (faceTarget) {
|
||||
Vec3fp toFace = faceTarget->position - position;
|
||||
toFace.y = fp(0);
|
||||
const fp distanceToFace = fpm::sqrt(toFace.normSquared());
|
||||
targetFacingAngle = distanceToFace > static_cast<fp>(1e-3f)
|
||||
? fpm::atan2(toFace.x / distanceToFace, -toFace.z / distanceToFace)
|
||||
: facingAngle;
|
||||
}
|
||||
else if (!faceTargetId.empty()) {
|
||||
faceTargetId.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!isPlayer && !navigationPausedThisTick && pathWaypoints.empty() &&
|
||||
(requestedWalkTarget - position).norm() > TARGET_REPLAN_THRESHOLD &&
|
||||
(requestedWalkTarget - position).normSquared() > TARGET_REPLAN_THRESHOLD_SQUARED &&
|
||||
!events.navigationRequested) {
|
||||
if (navigationRetryTicks > 0) --navigationRetryTicks;
|
||||
if (navigationRetryTicks == 0) {
|
||||
events.navigationRequested = true;
|
||||
events.forceNavigationReplan = true;
|
||||
events.navigationTarget = requestedWalkTarget;
|
||||
events.blockingCharacterId = blockingCharacterId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,9 +377,11 @@ CharacterUpdateEvents CharacterState::update(
|
||||
resetAnim = true;
|
||||
attackTargetId.clear();
|
||||
}
|
||||
} else if (battle_state == 1 && attackTarget) {
|
||||
targetFacingAngle = std::atan2(
|
||||
lookTarget.x() - position.x(), -(lookTarget.z() - position.z()));
|
||||
}
|
||||
else if (battle_state == 1 && attackTarget) {
|
||||
targetFacingAngle = fpm::atan2(
|
||||
attackTarget->position.x - position.x, -(attackTarget->position.z - position.z));
|
||||
|
||||
if (currentState == AnimationState::STAND || currentState == AnimationState::WALK) {
|
||||
currentState = AnimationState::STAND_TO_ACTION;
|
||||
resetAnim = true;
|
||||
@@ -388,24 +405,25 @@ CharacterUpdateEvents CharacterState::update(
|
||||
resetAnim = true;
|
||||
}
|
||||
|
||||
float angleDiff = targetFacingAngle - facingAngle;
|
||||
while (angleDiff > PI) angleDiff -= 2.f * PI;
|
||||
while (angleDiff < -PI) angleDiff += 2.f * PI;
|
||||
const float rotationStep = rotationSpeed * static_cast<float>(deltaMs) / 1000.f;
|
||||
if (std::fabs(angleDiff) <= rotationStep) {
|
||||
fp angleDiff = targetFacingAngle - facingAngle;
|
||||
const fp PIfp = static_cast<fp>(PI);
|
||||
while (angleDiff > PIfp) angleDiff -= static_cast<fp>(2.f) * PIfp;
|
||||
while (angleDiff < -PIfp) angleDiff += static_cast<fp>(2.f) * PIfp;
|
||||
const fp rotationStep = rotationSpeed * static_cast<fp>(deltaMs) / static_cast<fp>(1000.f);
|
||||
if (fpm::abs(angleDiff) <= rotationStep) {
|
||||
facingAngle = targetFacingAngle;
|
||||
faceTargetId.clear();
|
||||
} else {
|
||||
facingAngle += angleDiff > 0.f ? rotationStep : -rotationStep;
|
||||
facingAngle += angleDiff > static_cast<fp>(0.f) ? rotationStep : -rotationStep;
|
||||
}
|
||||
|
||||
if (!isPlayer && hp > 0.f && battle_state == 0 && maxHomeDrift > 0.f) {
|
||||
homeDriftCheckTimer -= static_cast<float>(deltaMs) / 1000.f;
|
||||
if (homeDriftCheckTimer <= 0.f) {
|
||||
homeDriftCheckTimer = HOME_DRIFT_CHECK_INTERVAL;
|
||||
Eigen::Vector3f toHome = homePosition - position;
|
||||
toHome.y() = 0.f;
|
||||
if (!isMoving() && toHome.norm() > maxHomeDrift) {
|
||||
if (!isPlayer && hp > 0.f && battle_state == 0 && maxHomeDriftSquared > static_cast<fp>(0.f)) {
|
||||
homeDriftCheckTimer -= static_cast<fp>(deltaMs) / static_cast<fp>(1000.f);
|
||||
if (homeDriftCheckTimer <= static_cast <fp>(0.f)) {
|
||||
homeDriftCheckTimer = HOME_DRIFT_CHECK_INTERVAL_FP;
|
||||
Vec3fp toHome = homePosition - position;
|
||||
toHome.y = static_cast<fp>(0.f);
|
||||
if (!isMoving() && toHome.normSquared() > maxHomeDriftSquared) {
|
||||
events.navigationRequested = true;
|
||||
events.navigationTarget = homePosition;
|
||||
}
|
||||
@@ -428,10 +446,18 @@ CharacterUpdateEvents CharacterState::update(
|
||||
events.attackImpact = true;
|
||||
events.attackTargetId = attackTargetId;
|
||||
events.attackDirection = attackTarget->position - position;
|
||||
events.attackDirection.y() = 0.f;
|
||||
const float length = events.attackDirection.norm();
|
||||
if (length > 1e-3f) events.attackDirection /= length;
|
||||
else events.attackDirection = Eigen::Vector3f::Zero();
|
||||
events.attackDirection.y = static_cast<fp>(0.f);
|
||||
const fp lengthSquared = events.attackDirection.normSquared();
|
||||
if (lengthSquared > static_cast<fp>(0.001f))
|
||||
{
|
||||
const fp length = fpm::sqrt(lengthSquared); //FP SQRT
|
||||
|
||||
events.attackDirection = events.attackDirection / length;
|
||||
}
|
||||
else
|
||||
{
|
||||
events.attackDirection = Vec3fp{};
|
||||
}
|
||||
}
|
||||
|
||||
showWeapon =
|
||||
@@ -467,9 +493,9 @@ CharacterUpdateEvents CharacterState::update(
|
||||
|
||||
void CharacterState::save(nlohmann::json& out) const
|
||||
{
|
||||
out["position"] = { position.x(), position.y(), position.z() };
|
||||
out["facingAngle"] = facingAngle;
|
||||
out["targetFacingAngle"] = targetFacingAngle;
|
||||
out["position"] = { static_cast<double>(position.x), static_cast<double>(position.y), static_cast<double>(position.z) };
|
||||
out["facingAngle"] = static_cast<double>(facingAngle);
|
||||
out["targetFacingAngle"] = static_cast<double>(targetFacingAngle);
|
||||
out["hp"] = hp;
|
||||
out["enabled"] = enabled;
|
||||
out["battle_state"] = battle_state;
|
||||
@@ -478,25 +504,24 @@ void CharacterState::save(nlohmann::json& out) const
|
||||
out["showWeapon"] = showWeapon;
|
||||
out["attackTargetId"] = attackTargetId;
|
||||
out["faceTargetId"] = faceTargetId;
|
||||
out["homePosition"] = { homePosition.x(), homePosition.y(), homePosition.z() };
|
||||
out["walkTarget"] = { walkTarget.x(), walkTarget.y(), walkTarget.z() };
|
||||
out["requestedWalkTarget"] = { requestedWalkTarget.x(), requestedWalkTarget.y(), requestedWalkTarget.z() };
|
||||
out["homePosition"] = { static_cast<double>(homePosition.x), static_cast<double>(homePosition.y), static_cast<double>(homePosition.z) };
|
||||
out["walkTarget"] = { static_cast<double>(walkTarget.x), static_cast<double>(walkTarget.y), static_cast<double>(walkTarget.z) };
|
||||
out["requestedWalkTarget"] = { static_cast<double>(requestedWalkTarget.x), static_cast<double>(requestedWalkTarget.y), static_cast<double>(requestedWalkTarget.z) };
|
||||
nlohmann::json wpArr = nlohmann::json::array();
|
||||
for (const auto& wp : pathWaypoints) {
|
||||
wpArr.push_back({ wp.x(), wp.y(), wp.z() });
|
||||
wpArr.push_back({ static_cast<double>(wp.x), static_cast<double>(wp.y), static_cast<double>(wp.z) });
|
||||
}
|
||||
out["pathWaypoints"] = std::move(wpArr);
|
||||
out["currentWaypointIndex"] = currentWaypointIndex;
|
||||
out["homeDriftCheckTimer"] = homeDriftCheckTimer;
|
||||
out["homeDriftCheckTimer"] = static_cast<double>(homeDriftCheckTimer);
|
||||
out["movementBlockedTicks"] = movementBlockedTicks;
|
||||
out["navigationRetryTicks"] = navigationRetryTicks;
|
||||
out["movedLastTick"] = movedLastTick;
|
||||
out["navigationRetryCooldownTicks"] = navigationRetryCooldownTicks;
|
||||
out["navigationRetryCooldownIndex"] = navigationRetryCooldownIndex;
|
||||
if (!blockingCharacterId.empty())
|
||||
out["blockingCharacterId"] = blockingCharacterId;
|
||||
|
||||
out["animationFrame"] = animationFrame;
|
||||
out["visibleRange"] = visibleRange;
|
||||
out["visibleRange"] = static_cast<double>(visibleRange);
|
||||
if (!onArrivedCallbackName.empty())
|
||||
out["onArrivedCallbackName"] = onArrivedCallbackName;
|
||||
}
|
||||
@@ -509,11 +534,19 @@ void CharacterState::load(const nlohmann::json& in)
|
||||
return def;
|
||||
};
|
||||
|
||||
if (in.contains("position")) position = readVec3(in["position"], position);
|
||||
facingAngle = in.value("facingAngle", facingAngle);
|
||||
targetFacingAngle = in.value("targetFacingAngle", targetFacingAngle);
|
||||
auto readVec3fp = [](const nlohmann::json& j, Vec3fp def) -> Vec3fp {
|
||||
if (j.is_array() && j.size() == 3)
|
||||
return Vec3fp{ static_cast<fp>(j[0].get<double>()), static_cast<fp>(j[1].get<double>()), static_cast<fp>(j[2].get<double>()) };
|
||||
return def;
|
||||
};
|
||||
|
||||
if (in.contains("position")) {
|
||||
position = readVec3fp(in["position"], position);
|
||||
}
|
||||
facingAngle = static_cast<fp>(in.value("facingAngle", static_cast<double>(facingAngle)));
|
||||
targetFacingAngle = static_cast<fp>(in.value("targetFacingAngle", static_cast<double>(targetFacingAngle)));
|
||||
hp = in.value("hp", hp);
|
||||
visibleRange = in.value("visibleRange", visibleRange);
|
||||
visibleRange = static_cast<fp>(in.value("visibleRange", static_cast<double>(visibleRange)));
|
||||
enabled = in.value("enabled", enabled);
|
||||
battle_state = in.value("battle_state", battle_state);
|
||||
currentState = static_cast<AnimationState>(in.value("currentState", static_cast<int>(currentState)));
|
||||
@@ -521,17 +554,17 @@ void CharacterState::load(const nlohmann::json& in)
|
||||
showWeapon = in.value("showWeapon", showWeapon);
|
||||
attackTargetId = in.value("attackTargetId", attackTargetId);
|
||||
faceTargetId = in.value("faceTargetId", faceTargetId);
|
||||
if (in.contains("homePosition")) homePosition = readVec3(in["homePosition"], homePosition);
|
||||
if (in.contains("walkTarget")) walkTarget = readVec3(in["walkTarget"], walkTarget);
|
||||
if (in.contains("requestedWalkTarget")) requestedWalkTarget = readVec3(in["requestedWalkTarget"], requestedWalkTarget);
|
||||
if (in.contains("homePosition")) homePosition = readVec3fp(in["homePosition"], homePosition);
|
||||
if (in.contains("walkTarget")) walkTarget = readVec3fp(in["walkTarget"], walkTarget);
|
||||
if (in.contains("requestedWalkTarget")) requestedWalkTarget = readVec3fp(in["requestedWalkTarget"], requestedWalkTarget);
|
||||
pathWaypoints.clear();
|
||||
if (in.contains("pathWaypoints") && in["pathWaypoints"].is_array()) {
|
||||
for (const auto& wp : in["pathWaypoints"]) {
|
||||
pathWaypoints.push_back(readVec3(wp, Eigen::Vector3f::Zero()));
|
||||
pathWaypoints.push_back(readVec3fp(wp, Vec3fp()));
|
||||
}
|
||||
}
|
||||
currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex);
|
||||
homeDriftCheckTimer = in.value("homeDriftCheckTimer", homeDriftCheckTimer);
|
||||
homeDriftCheckTimer = static_cast<fp>(in.value("homeDriftCheckTimer", static_cast<double>(homeDriftCheckTimer)));
|
||||
movementBlockedTicks = in.value("movementBlockedTicks", movementBlockedTicks);
|
||||
navigationRetryTicks = in.value("navigationRetryTicks", navigationRetryTicks);
|
||||
movedLastTick = in.value("movedLastTick", movedLastTick);
|
||||
@@ -541,8 +574,6 @@ void CharacterState::load(const nlohmann::json& in)
|
||||
"navigationRetryCooldownIndex", navigationRetryCooldownIndex);
|
||||
if (!navigationRetryCooldownTicks.empty())
|
||||
navigationRetryCooldownIndex %= navigationRetryCooldownTicks.size();
|
||||
blockingCharacterId =
|
||||
in.value("blockingCharacterId", std::string());
|
||||
animationFrame = in.value("animationFrame", animationFrame);
|
||||
onArrivedCallbackName = in.value("onArrivedCallbackName", std::string());
|
||||
}
|
||||
|
||||
+35
-41
@@ -61,6 +61,9 @@ public:
|
||||
// Distance below which the character is considered to have reached its target.
|
||||
static constexpr float kWalkThreshold = 0.05f;
|
||||
|
||||
static fp kWalkThreshold_SQUARED() { return static_cast<fp>(kWalkThreshold * kWalkThreshold); };
|
||||
|
||||
|
||||
// --- Identity ---
|
||||
std::string npcName;
|
||||
bool isPlayer = false;
|
||||
@@ -68,15 +71,15 @@ public:
|
||||
bool canAttack = false;
|
||||
|
||||
// --- Position / orientation ---
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float facingAngle = 0.0f;
|
||||
float targetFacingAngle = 0.0f;
|
||||
Vec3fp position;
|
||||
fp facingAngle;
|
||||
fp targetFacingAngle;
|
||||
|
||||
// --- Health ---
|
||||
float hp = 200.f;
|
||||
float initialHp = 200.f;
|
||||
|
||||
float visibleRange = VISIBLE_RANGE;
|
||||
fp visibleRange = static_cast<fp>(VISIBLE_RANGE);
|
||||
|
||||
// --- Combat flags ---
|
||||
int battle_state = 0;
|
||||
@@ -89,14 +92,14 @@ public:
|
||||
bool showWeapon = true;
|
||||
|
||||
// --- Stable character references ---
|
||||
// Empty means no target. "player" is the campaign player; NPC IDs are
|
||||
// Empty means no target. "player1" is the campaign player; NPC IDs are
|
||||
// location-local and validated while loading location content.
|
||||
std::string attackTargetId;
|
||||
std::string faceTargetId;
|
||||
|
||||
// --- Home position for drift recovery ---
|
||||
Eigen::Vector3f homePosition = Eigen::Vector3f::Zero();
|
||||
float maxHomeDrift = 3.0f;
|
||||
Vec3fp homePosition;
|
||||
fp maxHomeDriftSquared = static_cast<fp>(3.f*3.f);
|
||||
|
||||
// --- Authoritative logical animation clock ---
|
||||
// Frame counts are immutable metadata copied from loaded clips during
|
||||
@@ -106,29 +109,33 @@ public:
|
||||
std::array<int, 9> animationFrameCounts = {{1, 1, 1, 1, 1, 1, 1, 1, 1}};
|
||||
|
||||
// --- Movement / visual parameters ---
|
||||
float walkSpeed = 3.0f;
|
||||
float rotationSpeed = 4.0f;
|
||||
fp walkSpeed = static_cast<fp>(3.0f);
|
||||
fp rotationSpeed = static_cast<fp>(4.0f);
|
||||
|
||||
float modelScale = 0.12f;
|
||||
Eigen::Quaternionf modelCorrectionRotation = Eigen::Quaternionf::Identity();
|
||||
bool useGpuSkinning = true;
|
||||
// Full clearance used while planning around stationary characters.
|
||||
float collisionRadius = 0.45f;
|
||||
// Smaller physical contact threshold used by the movement guard.
|
||||
float movementContactRadius = 0.32f;
|
||||
float interactionRadius = 0.0f;
|
||||
|
||||
fp collisionRadius = static_cast<fp>(0.4f); // Радиус капсулы персонажа
|
||||
|
||||
// Векторы скорости в плоскости XZ (сериализовать в JSON не обязательно, если считаются каждый тик,
|
||||
// но для строгого lockstep-снапшота velocity лучше сохранять)
|
||||
Vec2fp velocity{ fp(0), fp(0) }; // Реальная скорость с прошлого тика (нужна соседям в ORCA)
|
||||
Vec2fp preferredVelocity{ fp(0), fp(0) }; // Желаемая скорость к waypoint'у на текущем тике
|
||||
|
||||
fp interactionRadius = static_cast<fp>(0.0f);
|
||||
bool movedLastTick = false;
|
||||
|
||||
// --- Pathfinding data (serializable parts) ---
|
||||
Eigen::Vector3f walkTarget = Eigen::Vector3f::Zero();
|
||||
Eigen::Vector3f requestedWalkTarget = Eigen::Vector3f::Zero();
|
||||
std::vector<Eigen::Vector3f> pathWaypoints;
|
||||
Vec3fp walkTarget;
|
||||
Vec3fp requestedWalkTarget;
|
||||
std::vector<Vec3fp> pathWaypoints;
|
||||
size_t currentWaypointIndex = 0;
|
||||
|
||||
// Internal drift-check timer (serializable so it survives save/load).
|
||||
float homeDriftCheckTimer = 5.0f;
|
||||
fp homeDriftCheckTimer = static_cast<fp>(5.0f);
|
||||
int movementBlockedTicks = 0;
|
||||
int navigationRetryTicks = 0;
|
||||
std::string blockingCharacterId;
|
||||
std::vector<int> navigationRetryCooldownTicks;
|
||||
std::size_t navigationRetryCooldownIndex = 0;
|
||||
|
||||
@@ -145,19 +152,15 @@ public:
|
||||
const std::string& entityId,
|
||||
std::vector<int> cooldownTicks = {});
|
||||
bool isMoving() const;
|
||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; }
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||
const Vec3fp& getRequestedWalkTarget() const { return requestedWalkTarget; }
|
||||
Vec3fp getCurrentNavigationTarget() const;
|
||||
void stopInPlace();
|
||||
void setTarget(const Eigen::Vector3f& target,
|
||||
void setTarget(const Vec3fp& target,
|
||||
std::string callbackName,
|
||||
const LocationNetworkState& location,
|
||||
const std::string& entityId,
|
||||
const std::vector<PathFinder::DynamicObstacle>&
|
||||
dynamicObstacles = {});
|
||||
const std::string& entityId);
|
||||
void forceReplan(const LocationNetworkState& location,
|
||||
const std::string& entityId,
|
||||
const std::vector<PathFinder::DynamicObstacle>&
|
||||
dynamicObstacles = {});
|
||||
const std::string& entityId);
|
||||
void stopAndRotateToTarget(std::string characterId);
|
||||
void resetPlayerAfterDeath();
|
||||
void tryAttackCombo();
|
||||
@@ -177,23 +180,14 @@ using PlayerStateMap =
|
||||
std::unordered_map<std::string, std::shared_ptr<CharacterState>>;
|
||||
|
||||
struct CharacterObservation {
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
Vec3fp position;
|
||||
float hp = 0.f;
|
||||
};
|
||||
|
||||
struct CharacterMovementObstacle {
|
||||
std::string id;
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float radius = 0.f;
|
||||
bool moving = false;
|
||||
Eigen::Vector3f movementDirection = Eigen::Vector3f::Zero();
|
||||
};
|
||||
|
||||
struct CharacterUpdateContext {
|
||||
std::string characterId;
|
||||
const CharacterObservation* attackTarget = nullptr;
|
||||
const CharacterObservation* faceTarget = nullptr;
|
||||
const std::vector<CharacterMovementObstacle>* movementObstacles = nullptr;
|
||||
};
|
||||
|
||||
struct CharacterUpdateEvents {
|
||||
@@ -201,11 +195,11 @@ struct CharacterUpdateEvents {
|
||||
bool deathAnimationComplete = false;
|
||||
bool navigationRequested = false;
|
||||
bool forceNavigationReplan = false;
|
||||
Eigen::Vector3f navigationTarget = Eigen::Vector3f::Zero();
|
||||
std::string blockingCharacterId;
|
||||
Vec3fp navigationTarget;
|
||||
|
||||
std::string arrivedCallbackName;
|
||||
std::string attackTargetId;
|
||||
Eigen::Vector3f attackDirection = Eigen::Vector3f::Zero();
|
||||
Vec3fp attackDirection;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+6
-4
@@ -558,8 +558,10 @@ namespace FRG
|
||||
logger() << "[network] Campaign '" << campaignId
|
||||
<< "' selected; creating WebSocket client" << std::endl;
|
||||
// Use "ws://127.0.0.1:8080/" for a local development server.
|
||||
static const char* const kMultiplayerServerUrl =
|
||||
"wss://api.shadowoverbishkek.com/server001/";
|
||||
|
||||
static const char* const kMultiplayerServerUrl = "wss://api.shadowoverbishkek.com/server001/";
|
||||
//static const char* const kMultiplayerServerUrl = "ws://127.0.0.1:8080/";
|
||||
|
||||
WebsocketEndpoint endpoint;
|
||||
if (!endpoint.parse(kMultiplayerServerUrl)) {
|
||||
logger() << "[network] Invalid multiplayer server URL: "
|
||||
@@ -662,9 +664,9 @@ namespace FRG
|
||||
if (!loc) return;
|
||||
if (loc->player) {
|
||||
server().submit(SetCharacterPositionCommand{
|
||||
destName, loc->player->entityId(), destPos});
|
||||
destName, loc->player->entityId(), Vec3fp::fromEigen(destPos)});
|
||||
server().submit(SetCharacterFacingAngleCommand{
|
||||
destName, loc->player->entityId(), destRotY, true});
|
||||
destName, loc->player->entityId(), static_cast<fp>(destRotY), true});
|
||||
loc->stopCharacter(*loc->player);
|
||||
}
|
||||
loc->localState().cameraAzimuth = destRotY;
|
||||
|
||||
+26
-58
@@ -126,7 +126,7 @@ namespace FRG
|
||||
}
|
||||
|
||||
void Location::setCharacterTarget(
|
||||
Character& character, const Eigen::Vector3f& target, std::string callbackName)
|
||||
Character& character, const Vec3fp& target, std::string callbackName)
|
||||
{
|
||||
gameServer_.submit(SetCharacterDestinationCommand{
|
||||
locationId_, character.entityId(), target, std::move(callbackName)});
|
||||
@@ -851,8 +851,8 @@ namespace FRG
|
||||
float t0 = (worldMin[axis] - rayOrigin[axis]) * invD;
|
||||
float t1 = (worldMax[axis] - rayOrigin[axis]) * invD;
|
||||
if (invD < 0.0f) std::swap(t0, t1);
|
||||
tMin = max(tMin, t0);
|
||||
tMax = min(tMax, t1);
|
||||
tMin = std::max(tMin, t0);
|
||||
tMax = std::min(tMax, t1);
|
||||
if (tMax < tMin) { miss = true; break; }
|
||||
}
|
||||
}
|
||||
@@ -1597,7 +1597,7 @@ namespace FRG
|
||||
if (playerState && !pointLights.empty()) {
|
||||
float minDist = FLT_MAX;
|
||||
for (int i = 0; i < static_cast<int>(pointLights.size()); ++i) {
|
||||
const float d = (pointLights[i].position - playerState->position).norm();
|
||||
const float d = (pointLights[i].position - playerState->position.toEigen()).norm();
|
||||
if (d < minDist) { minDist = d; nearestLightIdx = i; }
|
||||
}
|
||||
}
|
||||
@@ -1611,12 +1611,12 @@ namespace FRG
|
||||
if (!active && playerState) {
|
||||
if (pl.autoLightLimitX > 0.0f && pl.autoLightLimitY > 0.0f) {
|
||||
// Rectangular activation zone — independent of nearest-light heuristic
|
||||
const float dx = std::abs(playerState->position.x() - pl.position.x());
|
||||
const float dz = std::abs(playerState->position.z() - pl.position.z());
|
||||
const float dx = std::abs(static_cast<float>(playerState->position.x) - pl.position.x());
|
||||
const float dz = std::abs(static_cast<float>(playerState->position.z) - pl.position.z());
|
||||
active = (dx <= pl.autoLightLimitX && dz <= pl.autoLightLimitY);
|
||||
} else {
|
||||
// only the nearest autoLight activates, with optional distance cap
|
||||
const float dist = (pl.position - playerState->position).norm();
|
||||
const float dist = (pl.position - playerState->position.toEigen()).norm();
|
||||
active = (i == nearestLightIdx) &&
|
||||
(pl.autoLightDistance <= 0.0f || dist <= pl.autoLightDistance);
|
||||
}
|
||||
@@ -1632,7 +1632,7 @@ namespace FRG
|
||||
if (playerState && !activeLightIndices.empty()) {
|
||||
float minDist = FLT_MAX;
|
||||
for (int idx : activeLightIndices) {
|
||||
const float d = (pointLights[idx].position - playerState->position).norm();
|
||||
const float d = (pointLights[idx].position - playerState->position.toEigen()).norm();
|
||||
if (d < minDist) { minDist = d; shadowLight = &pointLights[idx]; }
|
||||
}
|
||||
}
|
||||
@@ -1838,50 +1838,18 @@ namespace FRG
|
||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) {
|
||||
return false;
|
||||
}
|
||||
const auto& areas =
|
||||
state->navigationMaps[state->activeNavigationIndex].getAreas();
|
||||
const bool found = std::any_of(
|
||||
areas.begin(), areas.end(),
|
||||
[&](const PathFinder::NavigationArea& area) {
|
||||
return area.name == areaName;
|
||||
});
|
||||
if (!found) return false;
|
||||
const auto& area =
|
||||
state->navigationMaps[state->activeNavigationIndex].getArea();
|
||||
if (area.name != areaName)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
gameServer_.submit(SetNavigationAreaAvailableCommand{
|
||||
locationId_, areaName, available});
|
||||
return true;
|
||||
}
|
||||
|
||||
void Location::nudgeCharacterAside(Character* standing, const Eigen::Vector3f& awayFrom)
|
||||
{
|
||||
auto standingState = standing ? standing->stateSnapshot() : nullptr;
|
||||
if (!standingState || standingState->isPlayer) return;
|
||||
auto locationState = stateSnapshot();
|
||||
if (!locationState || locationState->activeNavigationIndex < 0 ||
|
||||
locationState->activeNavigationIndex >=
|
||||
static_cast<int>(locationState->navigationMaps.size())) return;
|
||||
const PathFinder& navigation =
|
||||
locationState->navigationMaps[locationState->activeNavigationIndex];
|
||||
if (navigation.isReady()) return;
|
||||
|
||||
static constexpr float kNudgeDist = 1.2f;
|
||||
|
||||
Eigen::Vector3f dir = standingState->position - awayFrom;
|
||||
dir.y() = 0.f;
|
||||
if (dir.norm() < 1e-3f) dir = Eigen::Vector3f(1.f, 0.f, 0.f);
|
||||
else dir.normalize();
|
||||
|
||||
const float angles[] = { 0.f, static_cast<float>(M_PI * 0.5), static_cast<float>(-M_PI * 0.5), static_cast<float>(M_PI) };
|
||||
for (float angle : angles) {
|
||||
Eigen::Vector3f rotated = Eigen::AngleAxisf(angle, Eigen::Vector3f::UnitY()) * dir;
|
||||
Eigen::Vector3f candidate = standingState->position + rotated * kNudgeDist;
|
||||
candidate.y() = 0.f;
|
||||
if (navigation.isWalkable(candidate)) {
|
||||
setCharacterTarget(*standing, candidate);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Location::update(int64_t delta, const LocationSimulationEvents& events)
|
||||
{
|
||||
synchronizeRemotePlayers();
|
||||
@@ -1952,7 +1920,7 @@ namespace FRG
|
||||
player->update(delta, eventFor(gameServer_.localPlayerId()));
|
||||
auto playerState = player->stateSnapshot();
|
||||
if (playerState && playerState->enabled)
|
||||
updateTriggerZones(playerState->position);
|
||||
updateTriggerZones(playerState->position.toEigen());
|
||||
}
|
||||
for (auto& [id, remote] : remotePlayers_)
|
||||
if (remote) remote->update(delta, eventFor(id));
|
||||
@@ -1989,7 +1957,7 @@ namespace FRG
|
||||
auto playerState = player ? player->stateSnapshot() : nullptr;
|
||||
if (auto* tz = getTargetTeleportZone(*locationState);
|
||||
tz && playerState && playerState->enabled) {
|
||||
float dist = (playerState->position - tz->position).norm();
|
||||
float dist = (playerState->position.toEigen() - tz->position).norm();
|
||||
if (dist <= tz->radius) {
|
||||
logger() << "[TELEPORT] Player reached teleport zone '" << tz->id << "'" << std::endl;
|
||||
gameServer_.submit(SetLocationTargetsCommand{
|
||||
@@ -2006,7 +1974,7 @@ namespace FRG
|
||||
for (auto& tz : teleportZones) {
|
||||
if (!tz.active || !tz.automatic) continue;
|
||||
const float autoR = tz.automaticRadius > 0.0f ? tz.automaticRadius : tz.radius;
|
||||
const float dist = (playerState->position - tz.position).norm();
|
||||
const float dist = (playerState->position.toEigen() - tz.position).norm();
|
||||
|
||||
if (!tz.automaticInitialized) {
|
||||
// If the player starts inside the zone (e.g. just arrived here via teleport),
|
||||
@@ -2102,7 +2070,7 @@ namespace FRG
|
||||
const Eigen::Vector3f interactionTarget = clickedObject->state.hasInteractionPosition
|
||||
? clickedObject->state.interactionPosition
|
||||
: clickedObject->state.position;
|
||||
setCharacterTarget(*player, interactionTarget);
|
||||
setCharacterTarget(*player, Vec3fp::fromEigen(interactionTarget));
|
||||
pathVisualization.start(*player, interactionTarget);
|
||||
clearCharacterAttackTarget(*player);
|
||||
//logger() << "[CLICK] Player moving to object..." << std::endl;
|
||||
@@ -2115,7 +2083,7 @@ namespace FRG
|
||||
if (clickedNpc && player) {
|
||||
auto clickedNpcState = clickedNpc->stateSnapshot();
|
||||
if (!clickedNpcState) return;
|
||||
float distance = (playerState->position - clickedNpcState->position).norm();
|
||||
float distance = (playerState->position - clickedNpcState->position).toEigen().norm();
|
||||
int npcIndex = -1;
|
||||
for (size_t i = 0; i < npcs.size(); ++i) {
|
||||
if (npcs[i].get() == clickedNpc) {
|
||||
@@ -2145,11 +2113,11 @@ namespace FRG
|
||||
setCharacterTarget(*player, clickedNpcState->position);
|
||||
pathVisualization.start(
|
||||
*player,
|
||||
clickedNpcState->position,
|
||||
clickedNpcState->position.toEigen(),
|
||||
PathVisualization::Style::Hostile);
|
||||
}
|
||||
|
||||
if (distance <= clickedNpcState->interactionRadius) {
|
||||
if (distance <= static_cast<float>(clickedNpcState->interactionRadius)) {
|
||||
//logger() << "[CLICK] Hostile NPC " << npcIndex << " in range; firing on_npc_interact" << std::endl;
|
||||
scriptEngine.callNpcInteractCallback(clickedNpc->entityId());
|
||||
}
|
||||
@@ -2173,7 +2141,7 @@ namespace FRG
|
||||
//logger() << "[CLICK] NPC " << npcIndex << " out of talk range (distance " << distance
|
||||
// << " > " << NPC_TALK_DISTANCE << "); walking to NPC..." << std::endl;
|
||||
setCharacterTarget(*player, clickedNpcState->position);
|
||||
pathVisualization.start(*player, clickedNpcState->position);
|
||||
pathVisualization.start(*player, clickedNpcState->position.toEigen());
|
||||
gameServer_.submit(SetLocationTargetsCommand{
|
||||
locationId_, std::string(), clickedNpc->entityId(),
|
||||
std::string()});
|
||||
@@ -2200,7 +2168,7 @@ namespace FRG
|
||||
gameServer_.submit(SetLocationTargetsCommand{
|
||||
locationId_, std::string(), std::string(),
|
||||
clickedTeleport->id});
|
||||
setCharacterTarget(*player, clickedTeleport->position);
|
||||
setCharacterTarget(*player, Vec3fp::fromEigen(clickedTeleport->position));
|
||||
pathVisualization.start(
|
||||
*player,
|
||||
clickedTeleport->position,
|
||||
@@ -2208,7 +2176,7 @@ namespace FRG
|
||||
clearCharacterAttackTarget(*player);
|
||||
} else {
|
||||
const Eigen::Vector3f clickedDestination(hit.x(), 0.f, hit.z());
|
||||
setCharacterTarget(*player, clickedDestination);
|
||||
setCharacterTarget(*player, Vec3fp::fromEigen(clickedDestination));
|
||||
pathVisualization.start(*player, clickedDestination);
|
||||
clearCharacterAttackTarget(*player);
|
||||
gameServer_.submit(SetLocationTargetsCommand{
|
||||
@@ -2253,7 +2221,7 @@ namespace FRG
|
||||
const float minInclination = M_PI * 30.f / 180.f;
|
||||
const float maxInclination = M_PI * 0.5f;
|
||||
localState_->cameraInclination =
|
||||
max(minInclination, min(maxInclination, inclination));
|
||||
std::max(minInclination, std::min(maxInclination, inclination));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-3
@@ -64,7 +64,7 @@ namespace FRG
|
||||
std::string teleportsJsonPath;
|
||||
std::string triggerZonesJsonPath;
|
||||
std::string lightsJsonPath;
|
||||
Eigen::Vector3f playerPosition = Eigen::Vector3f::Zero();
|
||||
Vec3fp playerPosition;
|
||||
// Player shared by all locations of a campaign (see Location::createPlayer);
|
||||
// setup() creates a private one when this is null.
|
||||
std::shared_ptr<Character> player;
|
||||
@@ -174,7 +174,7 @@ namespace FRG
|
||||
void setup(const LocationSetup& params, AudioPlayerAsync* audioPlayer);
|
||||
//void setupNavigation(const std::vector<std::string>& paths);
|
||||
bool switchNavigation(int index);
|
||||
void setCharacterTarget(Character& character, const Eigen::Vector3f& target,
|
||||
void setCharacterTarget(Character& character, const Vec3fp& target,
|
||||
std::string callbackName = "");
|
||||
void clearCharacterAttackTarget(Character& character);
|
||||
void stopCharacter(Character& character);
|
||||
@@ -259,7 +259,6 @@ namespace FRG
|
||||
void loadTriggerZones(const std::string& jsonPath, const char* zipFile);
|
||||
void loadPointLights(const std::string& jsonPath, const char* zipFile);
|
||||
void updateTriggerZones(const Eigen::Vector3f& playerPos);
|
||||
void nudgeCharacterAside(Character* standing, const Eigen::Vector3f& awayFrom);
|
||||
void captureAuxiliaryState(LocationGameState& out) const;
|
||||
void applyAuxiliaryState(const LocationGameState& in);
|
||||
|
||||
|
||||
+11
-11
@@ -30,12 +30,12 @@ namespace FRG
|
||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
||||
const PathFinder& navigation =
|
||||
state->navigationMaps[state->activeNavigationIndex];
|
||||
const float y = navigation.getFloorY() + 0.02f;
|
||||
const float y = static_cast<float>(navigation.getFloorY()) + 0.02f;
|
||||
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
||||
for (const auto& obs : navigation.getObstaclePolygons()) {
|
||||
if (obs.polygon.size() < 3) continue;
|
||||
VertexRenderStruct mesh;
|
||||
mesh.data = CreatePolygonFloor(obs.polygon, y, red);
|
||||
mesh.data = CreatePolygonFloor(toEigenVector(obs.polygon), y, red);
|
||||
mesh.RefreshVBO();
|
||||
navigationEditorNavMeshes.push_back(std::move(mesh));
|
||||
}
|
||||
@@ -60,7 +60,7 @@ namespace FRG
|
||||
if (!state || state->activeNavigationIndex < 0 ||
|
||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
||||
const float y =
|
||||
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.05f;
|
||||
static_cast<float>(state->navigationMaps[state->activeNavigationIndex].getFloorY()) + 0.05f;
|
||||
const float s = 0.2f;
|
||||
|
||||
for (const auto& pt : navigationEditorPoints) {
|
||||
@@ -133,7 +133,7 @@ namespace FRG
|
||||
PathFinder::ObstaclePolygon poly;
|
||||
poly.name = "editor_obstacle_" + std::to_string(++navigationEditorObstacleCounter);
|
||||
for (const auto& pt : navigationEditorPoints) {
|
||||
poly.polygon.emplace_back(pt.x(), pt.z());
|
||||
poly.polygon.emplace_back(static_cast<FRG::fp>(pt.x()), static_cast<FRG::fp>(pt.z()));
|
||||
}
|
||||
|
||||
loc.gameServer_.submit(AddNavigationObstacleCommand{
|
||||
@@ -146,10 +146,10 @@ namespace FRG
|
||||
if (!state || state->activeNavigationIndex < 0 ||
|
||||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
|
||||
const float y =
|
||||
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.02f;
|
||||
static_cast<float>(state->navigationMaps[state->activeNavigationIndex].getFloorY()) + 0.02f;
|
||||
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
||||
VertexRenderStruct mesh;
|
||||
mesh.data = CreatePolygonFloor(poly.polygon, y, red);
|
||||
mesh.data = CreatePolygonFloor(toEigenVector(poly.polygon), y, red);
|
||||
mesh.RefreshVBO();
|
||||
navigationEditorNavMeshes.push_back(std::move(mesh));
|
||||
}
|
||||
@@ -379,9 +379,9 @@ namespace FRG
|
||||
|
||||
auto& target = loc.interactiveObjects[selectedInteractiveObjectIndex];
|
||||
target.state.boundsMin = Eigen::Vector3f(
|
||||
min(localA.x(), localB.x()), 0.f, min(localA.z(), localB.z()));
|
||||
std::min(localA.x(), localB.x()), 0.f, std::min(localA.z(), localB.z()));
|
||||
target.state.boundsMax = Eigen::Vector3f(
|
||||
max(localA.x(), localB.x()), 1.f, max(localA.z(), localB.z()));
|
||||
std::max(localA.x(), localB.x()), 1.f, std::max(localA.z(), localB.z()));
|
||||
|
||||
boundsClickCount = 0;
|
||||
buildInteractiveObjectBoundsMeshes();
|
||||
@@ -484,9 +484,9 @@ namespace FRG
|
||||
data.rotationZ = 0.0f;
|
||||
auto playerState = loc.player ? loc.player->stateSnapshot() : nullptr;
|
||||
if (!playerState) return;
|
||||
data.positionX = playerState->position.x();
|
||||
data.positionY = playerState->position.y();//-0.1;
|
||||
data.positionZ = playerState->position.z();
|
||||
data.positionX = static_cast<float>(playerState->position.x);
|
||||
data.positionY = static_cast<float>(playerState->position.y);
|
||||
data.positionZ = static_cast<float>(playerState->position.z);
|
||||
data.scale = scaleDist(rng);
|
||||
|
||||
GameObjectData data2;
|
||||
|
||||
+137
-193
@@ -2,6 +2,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include "OrcaSolver.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
@@ -126,34 +127,9 @@ const CharacterState* LocationGameState::findCharacter(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<PathFinder::DynamicObstacle> LocationGameState::collectPathObstacles(
|
||||
const std::string& requesterId,
|
||||
const PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId) const
|
||||
{
|
||||
std::vector<std::pair<std::string, const CharacterState*>> characters;
|
||||
characters.reserve(players.size() + npcs.size());
|
||||
for (const auto& [id, character] : players)
|
||||
if (character) characters.emplace_back(id, character.get());
|
||||
for (const auto& [id, character] : npcs)
|
||||
if (character) characters.emplace_back(id, character.get());
|
||||
std::sort(characters.begin(), characters.end(),
|
||||
[](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; });
|
||||
|
||||
std::vector<PathFinder::DynamicObstacle> obstacles;
|
||||
obstacles.reserve(characters.size());
|
||||
for (const auto& [id, character] : characters) {
|
||||
if (id == requesterId || !character->enabled || character->hp <= 0.f)
|
||||
continue;
|
||||
if (character->movedLastTick && id != forcedObstacleId) continue;
|
||||
obstacles.push_back({character->position, character->collisionRadius});
|
||||
}
|
||||
return obstacles;
|
||||
}
|
||||
|
||||
bool LocationGameState::setCharacterDestination(
|
||||
const std::string& id,
|
||||
const Eigen::Vector3f& target,
|
||||
const Vec3fp& target,
|
||||
std::string callbackName,
|
||||
PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId)
|
||||
@@ -161,54 +137,18 @@ bool LocationGameState::setCharacterDestination(
|
||||
CharacterState* character = findCharacter(id, players);
|
||||
if (!character || !location) return false;
|
||||
character->setTarget(
|
||||
target, std::move(callbackName), *location, id,
|
||||
collectPathObstacles(id, players, forcedObstacleId));
|
||||
target, std::move(callbackName), *location, id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LocationGameState::replanCharacter(
|
||||
const std::string& id,
|
||||
PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId)
|
||||
PlayerStateMap& players)
|
||||
{
|
||||
CharacterState* character = findCharacter(id, players);
|
||||
if (!character || !location) return false;
|
||||
if (!forcedObstacleId.empty() && !character->isPlayer &&
|
||||
character->canAttack && character->hp > 0.f) {
|
||||
const std::string selectedTarget =
|
||||
selectNpcTarget(*character, players, false);
|
||||
if (selectedTarget != character->attackTargetId) {
|
||||
character->stopInPlace();
|
||||
character->attackTargetId = selectedTarget;
|
||||
character->battle_state = 0;
|
||||
character->attack = 0;
|
||||
}
|
||||
|
||||
if (!selectedTarget.empty()) {
|
||||
const CharacterState* target = findCharacter(selectedTarget, players);
|
||||
if (target) {
|
||||
character->setTarget(
|
||||
target->position, {}, *location, id,
|
||||
collectPathObstacles(id, players, forcedObstacleId));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
character->attackTargetId.clear();
|
||||
character->battle_state = 0;
|
||||
character->attack = 0;
|
||||
const Eigen::Vector3f homeTarget(
|
||||
character->homePosition.x(), 0.f, character->homePosition.z());
|
||||
if ((homeTarget - character->position).norm() >
|
||||
CharacterState::kWalkThreshold) {
|
||||
character->setTarget(
|
||||
homeTarget, {}, *location, id,
|
||||
collectPathObstacles(id, players, forcedObstacleId));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
character->forceReplan(
|
||||
*location, id, collectPathObstacles(id, players, forcedObstacleId));
|
||||
character->forceReplan(*location, id);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -231,14 +171,14 @@ std::string LocationGameState::selectNpcTarget(
|
||||
playerIds.push_back(id);
|
||||
std::sort(playerIds.begin(), playerIds.end());
|
||||
|
||||
const float visibleRangeSquared = npc.visibleRange * npc.visibleRange;
|
||||
const fp visibleRangeSquared = npc.visibleRange * npc.visibleRange;
|
||||
std::string selectedTarget;
|
||||
float nearestDistanceSquared = visibleRangeSquared;
|
||||
fp nearestDistanceSquared = visibleRangeSquared;
|
||||
for (const std::string& playerId : playerIds) {
|
||||
const Eigen::Vector3f delta =
|
||||
const Vec3fp delta =
|
||||
players.at(playerId)->position - npc.position;
|
||||
const float distanceSquared =
|
||||
delta.x() * delta.x() + delta.z() * delta.z();
|
||||
const fp distanceSquared =
|
||||
delta.x * delta.x + delta.z * delta.z;
|
||||
if (distanceSquared <= nearestDistanceSquared &&
|
||||
(selectedTarget.empty() ||
|
||||
distanceSquared < nearestDistanceSquared ||
|
||||
@@ -362,12 +302,19 @@ LocationSimulationEvents LocationGameState::update(
|
||||
std::int64_t deltaMs, PlayerStateMap& players, bool interactiveObjectsLocked)
|
||||
{
|
||||
LocationSimulationEvents result;
|
||||
|
||||
//Taxi cars update:
|
||||
result.taxiArrived = updateNpcCar(deltaMs);
|
||||
|
||||
//Interactive objects update:
|
||||
std::vector<std::string> objectIds;
|
||||
objectIds.reserve(interactiveObjects.size());
|
||||
for (const auto& [id, object] : interactiveObjects)
|
||||
{
|
||||
if (object) objectIds.push_back(id);
|
||||
}
|
||||
std::sort(objectIds.begin(), objectIds.end());
|
||||
|
||||
for (const std::string& id : objectIds) {
|
||||
InteractiveObjectUpdateEvents events =
|
||||
interactiveObjects.at(id)->update(deltaMs);
|
||||
@@ -376,35 +323,49 @@ LocationSimulationEvents LocationGameState::update(
|
||||
{id, std::move(events.onCompleteName), false, {}});
|
||||
}
|
||||
|
||||
// All characters observe the same tick-start state. Damage is applied only
|
||||
// after every state has advanced, so vector order cannot suppress an attack.
|
||||
//Character update:
|
||||
const fp dtSec = static_cast<fp>(deltaMs) / fp(1000);
|
||||
|
||||
// 1. Собираем всех персонажей в единый массив и сортируем по ID
|
||||
// (чтобы даже порядок вызовов CharacterState::update был строго детерминирован!)
|
||||
struct Entry {
|
||||
std::string id;
|
||||
CharacterState* state;
|
||||
};
|
||||
std::vector<Entry> allChars;
|
||||
allChars.reserve(players.size() + npcs.size());
|
||||
|
||||
for (auto& [id, ptr] : players) {
|
||||
if (ptr && ptr->enabled) allChars.push_back({ id, ptr.get() });
|
||||
}
|
||||
for (auto& [id, ptr] : npcs) {
|
||||
if (ptr && ptr->enabled) allChars.push_back({ id, ptr.get() });
|
||||
}
|
||||
std::sort(allChars.begin(), allChars.end(), [](const Entry& a, const Entry& b) {
|
||||
return a.id < b.id;
|
||||
});
|
||||
|
||||
std::unordered_map<std::string, CharacterObservation> observations;
|
||||
for (const auto& [id, player] : players)
|
||||
if (player && player->enabled)
|
||||
observations.emplace(
|
||||
id, CharacterObservation{player->position, player->hp});
|
||||
id, CharacterObservation{ player->position, player->hp });
|
||||
for (const auto& [id, npc] : npcs)
|
||||
if (npc && npc->enabled)
|
||||
observations.emplace(
|
||||
id, CharacterObservation{npc->position, npc->hp});
|
||||
id, CharacterObservation{ npc->position, npc->hp });
|
||||
|
||||
std::vector<std::string> playerIds;
|
||||
playerIds.reserve(players.size());
|
||||
for (const auto& [id, player] : players)
|
||||
if (player) playerIds.push_back(id);
|
||||
std::sort(playerIds.begin(), playerIds.end());
|
||||
|
||||
std::vector<std::string> npcIds;
|
||||
npcIds.reserve(npcs.size());
|
||||
for (const auto& [id, npc] : npcs)
|
||||
if (npc) npcIds.push_back(id);
|
||||
std::sort(npcIds.begin(), npcIds.end());
|
||||
|
||||
// Target selection is authoritative and runs before all characters observe
|
||||
// the tick. A valid target is a lock: it is retained until the player dies
|
||||
// or becomes disabled, even when the player leaves visibleRange.
|
||||
for (const std::string& id : npcIds) {
|
||||
CharacterState& npc = *npcs.at(id);
|
||||
for (auto& entry : allChars) {
|
||||
if (entry.state->isPlayer)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CharacterState& npc = *entry.state;
|
||||
if (!npc.enabled || !npc.canAttack || npc.hp <= 0.f)
|
||||
continue;
|
||||
|
||||
@@ -420,88 +381,105 @@ LocationSimulationEvents LocationGameState::update(
|
||||
if (selectedTarget.empty()) {
|
||||
npc.battle_state = 0;
|
||||
npc.attack = 0;
|
||||
const Eigen::Vector3f homeTarget(
|
||||
npc.homePosition.x(), 0.f, npc.homePosition.z());
|
||||
const Eigen::Vector3f toHome = homeTarget - npc.position;
|
||||
if (toHome.norm() > CharacterState::kWalkThreshold &&
|
||||
(npc.requestedWalkTarget - homeTarget).norm() >
|
||||
CharacterState::kWalkThreshold) {
|
||||
setCharacterDestination(id, homeTarget, {}, players);
|
||||
const Vec3fp homeTarget(
|
||||
npc.homePosition.x, static_cast<fp>(0.f), npc.homePosition.z);
|
||||
const Vec3fp toHome = homeTarget - npc.position;
|
||||
if (toHome.normSquared() > CharacterState::kWalkThreshold_SQUARED() &&
|
||||
(npc.requestedWalkTarget - homeTarget).normSquared() >
|
||||
CharacterState::kWalkThreshold_SQUARED()) {
|
||||
setCharacterDestination(entry.id, homeTarget, {}, players);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<CharacterMovementObstacle> movementObstacles;
|
||||
movementObstacles.reserve(playerIds.size() + npcIds.size());
|
||||
auto addMovementObstacle =
|
||||
[&movementObstacles](const std::string& id,
|
||||
const CharacterState& character) {
|
||||
Eigen::Vector3f direction =
|
||||
character.getCurrentNavigationTarget() - character.position;
|
||||
direction.y() = 0.f;
|
||||
const float length = direction.norm();
|
||||
if (length > CharacterState::kWalkThreshold) direction /= length;
|
||||
else direction = Eigen::Vector3f::Zero();
|
||||
movementObstacles.push_back({
|
||||
id,
|
||||
character.position,
|
||||
character.movementContactRadius,
|
||||
character.movedLastTick,
|
||||
direction});
|
||||
};
|
||||
for (const std::string& id : playerIds) {
|
||||
const CharacterState& character = *players.at(id);
|
||||
if (character.enabled && character.hp > 0.f)
|
||||
addMovementObstacle(id, character);
|
||||
}
|
||||
for (const std::string& id : npcIds) {
|
||||
const CharacterState& character = *npcs.at(id);
|
||||
if (character.enabled && character.hp > 0.f)
|
||||
addMovementObstacle(id, character);
|
||||
}
|
||||
// One global ID order also makes blocker selection deterministic.
|
||||
std::sort(
|
||||
movementObstacles.begin(), movementObstacles.end(),
|
||||
[](const CharacterMovementObstacle& lhs,
|
||||
const CharacterMovementObstacle& rhs) { return lhs.id < rhs.id; });
|
||||
|
||||
auto advance = [&](const std::string& id, CharacterState& state) {
|
||||
// 2. ФАЗА 1: Обновляем логику каждого персонажа (вычисляется preferredVelocity)
|
||||
for (auto& entry : allChars) {
|
||||
CharacterUpdateContext context;
|
||||
context.characterId = id;
|
||||
context.movementObstacles = &movementObstacles;
|
||||
auto attack = observations.find(state.attackTargetId);
|
||||
context.characterId = entry.id;
|
||||
|
||||
|
||||
auto attack = observations.find(entry.state->attackTargetId);
|
||||
if (attack != observations.end()) context.attackTarget = &attack->second;
|
||||
auto face = observations.find(state.faceTargetId);
|
||||
auto face = observations.find(entry.state->faceTargetId);
|
||||
if (face != observations.end()) context.faceTarget = &face->second;
|
||||
CharacterUpdateEvents events = state.update(deltaMs, context);
|
||||
auto reservation = std::lower_bound(
|
||||
movementObstacles.begin(), movementObstacles.end(), id,
|
||||
[](const CharacterMovementObstacle& obstacle,
|
||||
const std::string& value) { return obstacle.id < value; });
|
||||
if (reservation != movementObstacles.end() && reservation->id == id) {
|
||||
reservation->position = state.position;
|
||||
reservation->moving = state.movedLastTick;
|
||||
Eigen::Vector3f direction =
|
||||
state.getCurrentNavigationTarget() - state.position;
|
||||
direction.y() = 0.f;
|
||||
const float length = direction.norm();
|
||||
if (length > CharacterState::kWalkThreshold)
|
||||
reservation->movementDirection = direction / length;
|
||||
else
|
||||
reservation->movementDirection = Eigen::Vector3f::Zero();
|
||||
}
|
||||
|
||||
CharacterUpdateEvents events = entry.state->update(deltaMs, context);
|
||||
|
||||
if (!events.arrivedCallbackName.empty() || events.attackImpact ||
|
||||
events.deathAnimationComplete || events.navigationRequested) {
|
||||
result.characters.push_back({id, std::move(events)});
|
||||
result.characters.push_back({ entry.id, std::move(events) });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for (const std::string& id : playerIds)
|
||||
if (players.at(id)->enabled)
|
||||
advance(id, *players.at(id));
|
||||
for (const std::string& id : npcIds)
|
||||
if (npcs.at(id)->enabled)
|
||||
advance(id, *npcs.at(id));
|
||||
std::vector<OrcaAgent> orcaAgents;
|
||||
orcaAgents.reserve(allChars.size());
|
||||
|
||||
for (auto& entry : allChars) {
|
||||
CharacterState* st = entry.state;
|
||||
if (st->hp <= 0.f) continue; // Мертвые не участвуют в коллизиях
|
||||
|
||||
const bool wantsToMove = st->preferredVelocity.lengthSq() > fp(0.001f);
|
||||
const bool inCombat = (st->battle_state == 1);
|
||||
|
||||
OrcaAgent agent;
|
||||
agent.id = entry.id;
|
||||
agent.state = st;
|
||||
agent.pos = Vec2fp(st->position.x, st->position.z);
|
||||
agent.vel = st->velocity;
|
||||
agent.prefVel = st->preferredVelocity;
|
||||
agent.radius = st->collisionRadius;
|
||||
agent.maxSpeed = st->walkSpeed;
|
||||
agent.isStationary = (!wantsToMove || inCombat);
|
||||
agent.isGhost = (st->canAttack && !st->isPlayer);
|
||||
agent.constantSpeed = !agent.isGhost;
|
||||
|
||||
orcaAgents.push_back(agent);
|
||||
}
|
||||
|
||||
const PathFinder* activeNavMesh = nullptr;
|
||||
if (location && location->activeNavigationIndex >= 0 &&
|
||||
location->activeNavigationIndex < static_cast<int>(location->navigationMaps.size())) {
|
||||
activeNavMesh = &location->navigationMaps[location->activeNavigationIndex];
|
||||
}
|
||||
|
||||
OrcaSolver::step(orcaAgents, dtSec, activeNavMesh);
|
||||
|
||||
// 4. ФАЗА 3: Записываем новые позиции, скорости и обновляем анимацию/поворот
|
||||
for (const auto& agent : orcaAgents) {
|
||||
CharacterState* st = agent.state;
|
||||
const Vec2fp oldPos(st->position.x, st->position.z);
|
||||
const Vec2fp displacement = agent.pos - oldPos;
|
||||
const fp movedDistSq = displacement.lengthSq();
|
||||
|
||||
st->position.x = agent.pos.x;
|
||||
st->position.z = agent.pos.y;
|
||||
st->velocity = agent.newVel;
|
||||
|
||||
st->movedLastTick = (movedDistSq > fp(0.0005f));
|
||||
|
||||
// Если персонаж хотел идти, проверяем — продвинулся ли он или уперся в глухую пробку
|
||||
const bool wantedToMove = st->preferredVelocity.lengthSq() > fp(0.01f);
|
||||
if (wantedToMove) {
|
||||
if (st->movedLastTick) {
|
||||
st->movementBlockedTicks = 0;
|
||||
if (st->currentState == AnimationState::STAND) {
|
||||
st->currentState = AnimationState::WALK;
|
||||
st->resetAnim = true;
|
||||
}
|
||||
|
||||
if (agent.newVel.lengthSq() > fp(0.05f)) {
|
||||
st->targetFacingAngle = fpm::atan2(agent.newVel.x, -agent.newVel.y);
|
||||
}
|
||||
}
|
||||
else {
|
||||
st->movementBlockedTicks++;
|
||||
if (st->currentState == AnimationState::WALK) {
|
||||
st->currentState = AnimationState::STAND;
|
||||
st->resetAnim = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const CharacterSimulationEvent& source : result.characters) {
|
||||
if (!source.events.attackImpact) continue;
|
||||
@@ -515,41 +493,7 @@ LocationSimulationEvents LocationGameState::update(
|
||||
damage,
|
||||
target->hp,
|
||||
target->initialHp,
|
||||
source.events.attackDirection});
|
||||
}
|
||||
|
||||
auto player = players.find("player1");
|
||||
if (!interactiveObjectsLocked &&
|
||||
!location->targetInteractiveObjectId.empty() &&
|
||||
player != players.end() && player->second && player->second->enabled) {
|
||||
auto target = interactiveObjects.find(location->targetInteractiveObjectId);
|
||||
const auto object =
|
||||
target == interactiveObjects.end() ? nullptr : target->second;
|
||||
if (object && !object->isAnimating) {
|
||||
const Eigen::Vector3f& approachTarget = object->hasInteractionPosition
|
||||
? object->interactionPosition : object->position;
|
||||
if ((player->second->position - approachTarget).norm() <= object->approachRadius) {
|
||||
result.interactiveObjects.push_back(
|
||||
{location->targetInteractiveObjectId, {}, true,
|
||||
object->activateFunctionName});
|
||||
location->targetInteractiveObjectId.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!location->targetInteractNpcId.empty() &&
|
||||
player != players.end() && player->second && player->second->enabled &&
|
||||
npcs.count(location->targetInteractNpcId) != 0 &&
|
||||
npcs.at(location->targetInteractNpcId)) {
|
||||
const auto& npc = npcs.at(location->targetInteractNpcId);
|
||||
const float distance =
|
||||
(player->second->position - npc->position).norm();
|
||||
if (distance <= NPC_TALK_DISTANCE) {
|
||||
player->second->stopInPlace();
|
||||
npc->faceTargetId = "player1";
|
||||
result.npcInteractionId = location->targetInteractNpcId;
|
||||
location->targetInteractNpcId.clear();
|
||||
}
|
||||
source.events.attackDirection });
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -27,7 +27,7 @@ struct CharacterDamageEvent {
|
||||
float amount = 0.f;
|
||||
float hp = 0.f;
|
||||
float maxHp = 0.f;
|
||||
Eigen::Vector3f direction = Eigen::Vector3f::Zero();
|
||||
Vec3fp direction;
|
||||
};
|
||||
|
||||
struct InteractiveObjectSimulationEvent {
|
||||
@@ -64,20 +64,15 @@ struct LocationGameState {
|
||||
CharacterState* findCharacter(const std::string& id, PlayerStateMap& players);
|
||||
const CharacterState* findCharacter(
|
||||
const std::string& id, const PlayerStateMap& players) const;
|
||||
std::vector<PathFinder::DynamicObstacle> collectPathObstacles(
|
||||
const std::string& requesterId,
|
||||
const PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId = {}) const;
|
||||
bool setCharacterDestination(
|
||||
const std::string& id,
|
||||
const Eigen::Vector3f& target,
|
||||
const Vec3fp& target,
|
||||
std::string callbackName,
|
||||
PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId = {});
|
||||
bool replanCharacter(
|
||||
const std::string& id,
|
||||
PlayerStateMap& players,
|
||||
const std::string& forcedObstacleId = {});
|
||||
PlayerStateMap& players);
|
||||
void startTaxi();
|
||||
void resetTaxi();
|
||||
LocationSimulationEvents update(
|
||||
|
||||
@@ -123,7 +123,7 @@ void loadSinglePlayerPersistence(
|
||||
|
||||
|
||||
// Entity ownership stays above this navigation-only layer.
|
||||
std::vector<Eigen::Vector3f> LocationNetworkState::planPathExceptMyself(
|
||||
/**td::vector<Eigen::Vector3f> LocationNetworkState::planPathExceptMyself(
|
||||
const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end,
|
||||
const std::string& entityId,
|
||||
@@ -135,7 +135,7 @@ void loadSinglePlayerPersistence(
|
||||
return {};
|
||||
return navigationMaps[activeNavigationIndex].findPathToNearest(
|
||||
start, end, dynamicObstacles);
|
||||
}
|
||||
}*/
|
||||
|
||||
void LocationNetworkState::initNavigation(
|
||||
const std::vector<std::string>& paths, const std::string& zipFile)
|
||||
|
||||
+2
-2
@@ -41,11 +41,11 @@ struct LocationNetworkState : public ISaveable {
|
||||
//This value is populated from the JSON configs and should not be changed during the regular gameplay.
|
||||
//It might be changed only in the editor mode during development.
|
||||
std::vector<PathFinder> navigationMaps;
|
||||
std::vector<Eigen::Vector3f> planPathExceptMyself(
|
||||
/*std::vector<Eigen::Vector3f> planPathExceptMyself(
|
||||
const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end,
|
||||
const std::string& entityId,
|
||||
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles = {}) const;
|
||||
const std::vector<PathFinder::DynamicObstacle>& dynamicObstacles = {}) const;*/
|
||||
void initNavigation(
|
||||
const std::vector<std::string>& paths,
|
||||
const std::string& zipFile = std::string());
|
||||
|
||||
+6
-7
@@ -17,10 +17,6 @@
|
||||
|
||||
namespace FRG {
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
//extern Game* Game::s_instance;
|
||||
#endif
|
||||
|
||||
// Localization
|
||||
static const std::string EMPTY_LANGUAGE_RU = u8"(пусто)";
|
||||
static const std::string EMPTY_LANGUAGE_EN = "(empty)";
|
||||
@@ -81,7 +77,7 @@ namespace FRG {
|
||||
pushWord(currentWord);
|
||||
if (!currentLine.empty()) flushLine();
|
||||
|
||||
return max(1, lines);
|
||||
return std::max(1, lines);
|
||||
}
|
||||
|
||||
static std::string trimChatPreview(const std::string& msg, int maxChars = 22) {
|
||||
@@ -408,6 +404,9 @@ namespace FRG {
|
||||
const std::string& loc = snapshot->currentLocationId;
|
||||
if (loc == "uni_exterior") return snapshot->isDarklands ? hudUniExtDarkRoot : hudUniExtRoot;
|
||||
if (loc == "uni_interior") return snapshot->isDarklands ? hudUniIntDarkRoot : hudUniIntRoot;
|
||||
// Other locations (e.g. campaigns that start directly in Darklands) have no
|
||||
// dedicated HUD; reuse the Darklands one so the health bar is shown.
|
||||
if (snapshot->isDarklands && !loc.empty()) return hudUniExtDarkRoot;
|
||||
return hudDormRoot; // dorm, or a new game before the location is set
|
||||
}
|
||||
|
||||
@@ -2254,9 +2253,9 @@ namespace FRG {
|
||||
|
||||
float MenuManager::ToastEntry::currentAlpha() const {
|
||||
switch (state) {
|
||||
case State::FadeIn: return min(timer / TOAST_FADE_MS, 1.0f);
|
||||
case State::FadeIn: return std::min(timer / TOAST_FADE_MS, 1.0f);
|
||||
case State::Visible: return 1.0f;
|
||||
case State::FadeOut: return max(1.0f - timer / TOAST_FADE_MS, 0.0f);
|
||||
case State::FadeOut: return std::max(1.0f - timer / TOAST_FADE_MS, 0.0f);
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
#pragma once
|
||||
|
||||
#include "navigation/PathFinder.h"
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
// Линейное ограничение (полуплоскость допустимых скоростей ORCA)
|
||||
struct OrcaLine {
|
||||
Vec2fp point; // Точка на граничной прямой
|
||||
Vec2fp direction; // Единичный вектор направления прямой (допустимая полуплоскость СЛЕВА)
|
||||
};
|
||||
|
||||
struct OrcaAgent {
|
||||
std::string id;
|
||||
CharacterState* state = nullptr;
|
||||
Vec2fp pos;
|
||||
Vec2fp vel;
|
||||
Vec2fp prefVel;
|
||||
fp radius;
|
||||
fp maxSpeed;
|
||||
bool isStationary = false;
|
||||
bool isGhost = false; // Враги-духи (парят в воздухе, легко расталкиваются)
|
||||
bool constantSpeed = true; // Люди (ходят только с полной скоростью либо стоят)
|
||||
Vec2fp newVel;
|
||||
};
|
||||
|
||||
class OrcaSolver {
|
||||
public:
|
||||
// Горизонт упреждения 0.6 сек (~1.8 метра при скорости 3 м/с)
|
||||
static fp timeHorizon() { return fp(0.6f); }
|
||||
|
||||
static void step(
|
||||
std::vector<OrcaAgent>& agents,
|
||||
fp dtSec,
|
||||
const PathFinder* navMesh)
|
||||
{
|
||||
if (dtSec <= fp(0) || agents.empty()) return;
|
||||
|
||||
// 1. Обязательная сортировка по ID для 100% детерминизма Lockstep
|
||||
std::sort(agents.begin(), agents.end(), [](const OrcaAgent& a, const OrcaAgent& b) {
|
||||
return a.id < b.id;
|
||||
});
|
||||
|
||||
const fp invTimeHorizon = fp(1) / timeHorizon();
|
||||
const fp invDt = fp(1) / dtSec;
|
||||
const fp neighborDist = fp(4.0f);
|
||||
const fp neighborDistSq = neighborDist * neighborDist;
|
||||
|
||||
std::vector<OrcaLine> orcaLines;
|
||||
orcaLines.reserve(16);
|
||||
|
||||
// 2. Для каждого агента строим линии ORCA относительно соседей
|
||||
for (std::size_t i = 0; i < agents.size(); ++i) {
|
||||
OrcaAgent& agent = agents[i];
|
||||
if (agent.maxSpeed <= fp(0) || agent.prefVel.lengthSq() <= fp(0.001f)) {
|
||||
agent.newVel = Vec2fp(fp(0), fp(0));
|
||||
continue;
|
||||
}
|
||||
|
||||
orcaLines.clear();
|
||||
|
||||
// ВАЖНО: Если человек стартует с места, используем prefVel как базу для построения VO,
|
||||
// чтобы стоящие рядом NPC не запирали его в нулевой скорости!
|
||||
const Vec2fp effectiveAgentVel = (agent.constantSpeed && agent.vel.lengthSq() < fp(0.1f))
|
||||
? agent.prefVel
|
||||
: agent.vel;
|
||||
|
||||
for (std::size_t j = 0; j < agents.size(); ++j) {
|
||||
if (i == j) continue;
|
||||
const OrcaAgent& other = agents[j];
|
||||
|
||||
// Человек НЕ строит тормозящих линий ORCA об духов —
|
||||
// духи сами расступаются перед человеком и расталкиваются в resolveHardCollisions!
|
||||
if (!agent.isGhost && other.isGhost) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const Vec2fp relativePos = other.pos - agent.pos;
|
||||
const fp distSq = relativePos.lengthSq();
|
||||
if (distSq > neighborDistSq) continue;
|
||||
|
||||
const Vec2fp relativeVel = effectiveAgentVel - other.vel;
|
||||
// Для проверки конуса обхода используем чуть меньший радиус (85%),
|
||||
// чтобы персонажи легче протискивались мимо друг друга
|
||||
const fp combinedRadius = (agent.radius + other.radius) * fp(0.85f);
|
||||
const fp combinedRadiusSq = combinedRadius * combinedRadius;
|
||||
|
||||
OrcaLine line;
|
||||
Vec2fp u;
|
||||
|
||||
if (distSq > combinedRadiusSq) {
|
||||
const Vec2fp w = relativeVel - relativePos * invTimeHorizon;
|
||||
const fp wLengthSq = w.lengthSq();
|
||||
const fp dotProduct1 = w.dot(relativePos);
|
||||
|
||||
if (dotProduct1 < fp(0) && dotProduct1 * dotProduct1 > combinedRadiusSq * wLengthSq) {
|
||||
const fp wLength = fpm::sqrt(wLengthSq);
|
||||
const Vec2fp unitW = wLength > fp(0.001f) ? (w / wLength) : Vec2fp(fp(1), fp(0));
|
||||
line.direction = Vec2fp(unitW.y, -unitW.x);
|
||||
u = unitW * (combinedRadius * invTimeHorizon - wLength);
|
||||
}
|
||||
else {
|
||||
const fp leg = fpm::sqrt(std::max(fp(0), distSq - combinedRadiusSq));
|
||||
if (relativePos.cross(w) > fp(0)) {
|
||||
line.direction = Vec2fp(
|
||||
relativePos.x * leg - relativePos.y * combinedRadius,
|
||||
relativePos.x * combinedRadius + relativePos.y * leg
|
||||
) / distSq;
|
||||
}
|
||||
else {
|
||||
line.direction = Vec2fp(
|
||||
-(relativePos.x * leg + relativePos.y * combinedRadius),
|
||||
-(-relativePos.x * combinedRadius + relativePos.y * leg)
|
||||
) / distSq;
|
||||
}
|
||||
const fp dotProduct2 = relativeVel.dot(line.direction);
|
||||
u = line.direction * dotProduct2 - relativeVel;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const Vec2fp w = relativeVel - relativePos * invDt;
|
||||
const fp wLength = w.length();
|
||||
const Vec2fp unitW = wLength > fp(0.001f)
|
||||
? (w / wLength)
|
||||
: ((i < j) ? Vec2fp(fp(1), fp(0)) : Vec2fp(fp(-1), fp(0)));
|
||||
|
||||
line.direction = Vec2fp(unitW.y, -unitW.x);
|
||||
u = unitW * (combinedRadius * invDt - wLength);
|
||||
}
|
||||
|
||||
fp responsibility = fp(0.5f);
|
||||
if (agent.isGhost && !other.isGhost) {
|
||||
responsibility = fp(0.95f); // Дух берет 95% уклонения от человека
|
||||
}
|
||||
else if (other.isStationary && !agent.isStationary) {
|
||||
responsibility = fp(1.0f); // Обходящий берет 100% маневра мимо стоящего
|
||||
}
|
||||
else if (!other.isStationary && agent.isStationary) {
|
||||
responsibility = fp(0.0f);
|
||||
}
|
||||
|
||||
line.point = effectiveAgentVel + u * responsibility;
|
||||
orcaLines.push_back(line);
|
||||
}
|
||||
|
||||
// Решаем стандартную задачу ORCA
|
||||
Vec2fp resultVel = agent.prefVel;
|
||||
int lineFail = linearProgram2(orcaLines, agent.maxSpeed, agent.prefVel, false, resultVel);
|
||||
if (lineFail < static_cast<int>(orcaLines.size())) {
|
||||
linearProgram3(orcaLines, lineFail, agent.maxSpeed, resultVel);
|
||||
}
|
||||
|
||||
// Если это человек (constantSpeed == true), ищем угол обхода с сохранением полной скорости:
|
||||
const fp prefSpeedSq = agent.prefVel.lengthSq();
|
||||
if (agent.constantSpeed && prefSpeedSq > fp(0.01f)) {
|
||||
const fp prefSpeed = fpm::sqrt(prefSpeedSq);
|
||||
resultVel = findConstantSpeedVelocity(orcaLines, agent.prefVel, prefSpeed, resultVel);
|
||||
}
|
||||
|
||||
agent.newVel = resultVel;
|
||||
}
|
||||
|
||||
// 3. Применяем скорости и корректируем жесткие наложения
|
||||
for (auto& agent : agents) {
|
||||
agent.pos = agent.pos + agent.newVel * dtSec;
|
||||
}
|
||||
|
||||
resolveHardCollisions(agents);
|
||||
|
||||
// 4. Удерживаем всех внутри NavMesh
|
||||
if (navMesh && navMesh->isReady()) {
|
||||
for (auto& agent : agents) {
|
||||
Vec3fp pt3D(agent.pos.x, navMesh->getFloorY(), agent.pos.y);
|
||||
if (!navMesh->isWalkable(pt3D)) {
|
||||
int dummyTri = -1;
|
||||
agent.pos = navMesh->clampToNavMesh(agent.pos, dummyTri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Подбирает вектор скорости строго длины prefSpeed, который огибает препятствие без замедления
|
||||
static Vec2fp findConstantSpeedVelocity(
|
||||
const std::vector<OrcaLine>& orcaLines,
|
||||
const Vec2fp& prefVel,
|
||||
fp prefSpeed,
|
||||
const Vec2fp& orcaVel)
|
||||
{
|
||||
if (orcaLines.empty()) return prefVel;
|
||||
|
||||
const Vec2fp forwardDir = prefVel / prefSpeed;
|
||||
// Перпендикуляр для отступа влево/вправо при лобовом столкновении
|
||||
const Vec2fp rightDir(forwardDir.y, -forwardDir.x);
|
||||
|
||||
// Перебираем веер углов отклонения от прямого курса (0°, ±15°, ±30°, ±45°, ±60°, ±75°, ±90°)
|
||||
// Синусы и косинусы заданы константами для скорости и детерминизма
|
||||
struct AngleOffset { fp cosA; fp sinA; };
|
||||
static const AngleOffset offsets[] = {
|
||||
{ fp(1.000f), fp(0.000f) },
|
||||
{ fp(0.966f), fp(0.259f) }, { fp(0.966f), fp(-0.259f) }, // ±15°
|
||||
{ fp(0.866f), fp(0.500f) }, { fp(0.866f), fp(-0.500f) }, // ±30°
|
||||
{ fp(0.707f), fp(0.707f) }, { fp(0.707f), fp(-0.707f) }, // ±45°
|
||||
{ fp(0.500f), fp(0.866f) }, { fp(0.500f), fp(-0.866f) }, // ±60°
|
||||
{ fp(0.259f), fp(0.966f) }, { fp(0.259f), fp(-0.966f) }, // ±75°
|
||||
{ fp(0.000f), fp(1.000f) }, { fp(0.000f), fp(-1.000f) } // ±90°
|
||||
};
|
||||
|
||||
// Определяем, в какую сторону (влево или вправо) нам выгоднее обходить по подсказке от ORCA
|
||||
const fp sideHint = rightDir.dot(orcaVel);
|
||||
|
||||
for (std::size_t k = 0; k < sizeof(offsets) / sizeof(offsets[0]); ++k) {
|
||||
// Если k нечетный (пара ±угол), проверяем сначала ту сторону, куда склоняется ORCA
|
||||
std::size_t idx = k;
|
||||
if (k >= 1 && (k % 2 == 1) && sideHint < fp(0)) {
|
||||
idx = k + 1;
|
||||
}
|
||||
else if (k >= 1 && (k % 2 == 0) && sideHint < fp(0)) {
|
||||
idx = k - 1;
|
||||
}
|
||||
|
||||
const Vec2fp candidateDir = forwardDir * offsets[idx].cosA + rightDir * offsets[idx].sinA;
|
||||
const Vec2fp candidateVel = candidateDir * prefSpeed;
|
||||
|
||||
bool valid = true;
|
||||
for (const auto& line : orcaLines) {
|
||||
// Допускаем погрешность 0.1f м/с
|
||||
if (line.direction.cross(line.point - candidateVel) > fp(0.1f)) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
return candidateVel;
|
||||
}
|
||||
}
|
||||
|
||||
// Если ни один угол до ±90° не свободен (проход полностью заблокирован вплотную) — стоим
|
||||
return Vec2fp(fp(0), fp(0));
|
||||
}
|
||||
|
||||
static void resolveHardCollisions(std::vector<OrcaAgent>& agents) {
|
||||
const std::size_t n = agents.size();
|
||||
for (int iter = 0; iter < 2; ++iter) {
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
for (std::size_t j = i + 1; j < n; ++j) {
|
||||
Vec2fp delta = agents[j].pos - agents[i].pos;
|
||||
fp distSq = delta.lengthSq();
|
||||
fp minDist = agents[i].radius + agents[j].radius;
|
||||
|
||||
if (distSq < minDist * minDist) {
|
||||
fp dist = fpm::sqrt(distSq);
|
||||
Vec2fp normal = (dist > fp(0.001f))
|
||||
? (delta / dist)
|
||||
: Vec2fp(fp(1), fp(0));
|
||||
|
||||
fp penetration = (minDist - dist) * fp(0.5f);
|
||||
|
||||
fp weightI = fp(0.5f);
|
||||
fp weightJ = fp(0.5f);
|
||||
|
||||
if (!agents[i].isGhost && agents[j].isGhost) {
|
||||
weightI = fp(0.0f); weightJ = fp(1.0f); // Человек на 100% выталкивает духа
|
||||
}
|
||||
else if (agents[i].isGhost && !agents[j].isGhost) {
|
||||
weightI = fp(1.0f); weightJ = fp(0.0f);
|
||||
}
|
||||
else {
|
||||
weightI = agents[i].isStationary ? fp(0.1f) : fp(0.5f);
|
||||
weightJ = agents[j].isStationary ? fp(0.1f) : fp(0.5f);
|
||||
fp sumW = weightI + weightJ;
|
||||
weightI /= sumW;
|
||||
weightJ /= sumW;
|
||||
}
|
||||
|
||||
agents[i].pos = agents[i].pos - normal * (penetration * weightI);
|
||||
agents[j].pos = agents[j].pos + normal * (penetration * weightJ);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool linearProgram1(
|
||||
const std::vector<OrcaLine>& lines,
|
||||
int lineNo,
|
||||
fp radius,
|
||||
const Vec2fp& optVelocity,
|
||||
bool directionOpt,
|
||||
Vec2fp& result)
|
||||
{
|
||||
const fp dotProduct = lines[lineNo].point.dot(lines[lineNo].direction);
|
||||
const fp discriminant = dotProduct * dotProduct + radius * radius - lines[lineNo].point.lengthSq();
|
||||
|
||||
if (discriminant < fp(0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fp sqrtDiscriminant = fpm::sqrt(discriminant);
|
||||
fp tLeft = -dotProduct - sqrtDiscriminant;
|
||||
fp tRight = -dotProduct + sqrtDiscriminant;
|
||||
|
||||
for (int i = 0; i < lineNo; ++i) {
|
||||
const fp denominator = lines[lineNo].direction.cross(lines[i].direction);
|
||||
const fp numerator = lines[i].direction.cross(lines[lineNo].point - lines[i].point);
|
||||
|
||||
if (fpm::abs(denominator) <= fp(0.001f)) {
|
||||
if (numerator < fp(0)) return false;
|
||||
continue;
|
||||
}
|
||||
|
||||
const fp t = numerator / denominator;
|
||||
if (denominator >= fp(0)) {
|
||||
tRight = std::min(tRight, t);
|
||||
}
|
||||
else {
|
||||
tLeft = std::max(tLeft, t);
|
||||
}
|
||||
|
||||
if (tLeft > tRight) return false;
|
||||
}
|
||||
|
||||
if (directionOpt) {
|
||||
if (optVelocity.dot(lines[lineNo].direction) > fp(0)) {
|
||||
result = lines[lineNo].point + lines[lineNo].direction * tRight;
|
||||
}
|
||||
else {
|
||||
result = lines[lineNo].point + lines[lineNo].direction * tLeft;
|
||||
}
|
||||
}
|
||||
else {
|
||||
const fp t = lines[lineNo].direction.dot(optVelocity - lines[lineNo].point);
|
||||
const fp tClamped = std::max(tLeft, std::min(tRight, t));
|
||||
result = lines[lineNo].point + lines[lineNo].direction * tClamped;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int linearProgram2(
|
||||
const std::vector<OrcaLine>& lines,
|
||||
fp radius,
|
||||
const Vec2fp& optVelocity,
|
||||
bool directionOpt,
|
||||
Vec2fp& result)
|
||||
{
|
||||
if (directionOpt) {
|
||||
result = optVelocity * radius;
|
||||
}
|
||||
else if (optVelocity.lengthSq() > radius * radius) {
|
||||
result = (optVelocity / optVelocity.length()) * radius;
|
||||
}
|
||||
else {
|
||||
result = optVelocity;
|
||||
}
|
||||
|
||||
for (int i = 0; i < static_cast<int>(lines.size()); ++i) {
|
||||
if (lines[i].direction.cross(lines[i].point - result) > fp(0)) {
|
||||
const Vec2fp tempResult = result;
|
||||
if (!linearProgram1(lines, i, radius, optVelocity, directionOpt, result)) {
|
||||
result = tempResult;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return static_cast<int>(lines.size());
|
||||
}
|
||||
|
||||
static void linearProgram3(
|
||||
const std::vector<OrcaLine>& lines,
|
||||
int beginLine,
|
||||
fp radius,
|
||||
Vec2fp& result)
|
||||
{
|
||||
fp distance = fp(0);
|
||||
|
||||
for (int i = beginLine; i < static_cast<int>(lines.size()); ++i) {
|
||||
if (lines[i].direction.cross(lines[i].point - result) > distance) {
|
||||
std::vector<OrcaLine> projLines;
|
||||
projLines.reserve(i);
|
||||
|
||||
for (int j = 0; j < i; ++j) {
|
||||
OrcaLine line;
|
||||
fp determinant = lines[i].direction.cross(lines[j].direction);
|
||||
|
||||
if (fpm::abs(determinant) <= fp(0.001f)) {
|
||||
if (lines[i].direction.dot(lines[j].direction) > fp(0)) continue;
|
||||
line.point = (lines[i].point + lines[j].point) * fp(0.5f);
|
||||
}
|
||||
else {
|
||||
line.point = lines[i].point + lines[i].direction *
|
||||
(lines[j].direction.cross(lines[i].point - lines[j].point) / determinant);
|
||||
}
|
||||
|
||||
Vec2fp dirDiff = lines[j].direction - lines[i].direction;
|
||||
fp dirLen = dirDiff.length();
|
||||
if (dirLen <= fp(0.001f)) continue;
|
||||
|
||||
line.direction = dirDiff / dirLen;
|
||||
projLines.push_back(line);
|
||||
}
|
||||
|
||||
const Vec2fp tempResult = result;
|
||||
const Vec2fp optDir(-lines[i].direction.y, lines[i].direction.x);
|
||||
if (linearProgram2(projLines, radius, optDir, true, result) < static_cast<int>(projLines.size())) {
|
||||
result = tempResult;
|
||||
}
|
||||
|
||||
distance = lines[i].direction.cross(lines[i].point - result);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+20
-14
@@ -13,6 +13,9 @@
|
||||
|
||||
namespace FRG {
|
||||
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
|
||||
|
||||
namespace {
|
||||
|
||||
// Reverse-looks-up the global name a Lua function is registered under, by
|
||||
@@ -80,6 +83,7 @@ namespace FRG {
|
||||
ScriptEngine::~ScriptEngine() = default;
|
||||
|
||||
void ScriptEngine::init(Location* loc, AudioPlayerAsync* audioPlayer, const std::string& scriptPath) {
|
||||
logger() << "ScriptEngine::init with script path: " << scriptPath << std::endl;
|
||||
impl = std::make_unique<Impl>();
|
||||
sol::state& lua = impl->lua;
|
||||
|
||||
@@ -125,9 +129,9 @@ namespace FRG {
|
||||
std::cerr << "[script] npc_walk_to: on_arrived must be a global Lua function\n";
|
||||
}
|
||||
loc->gameServer().submit(SetCharacterHomePositionCommand{
|
||||
loc->locationId(), npc->entityId(), Eigen::Vector3f(x, 0.f, z)});
|
||||
loc->locationId(), npc->entityId(), Vec3fp(static_cast<fp>(x), static_cast<fp>(0.f), static_cast<fp>(z))});
|
||||
loc->setCharacterTarget(
|
||||
*npc, Eigen::Vector3f(x, y, z), std::move(cbName));
|
||||
*npc, Vec3fp(static_cast<fp>(x), static_cast<fp>(y), static_cast<fp>(z)), std::move(cbName));
|
||||
});
|
||||
|
||||
|
||||
@@ -139,16 +143,16 @@ namespace FRG {
|
||||
return;
|
||||
}
|
||||
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
logger() << "[script] npc_rotate_to: " << npcId << " angle " << angle << " degrees (" << rad << " radians)" << std::endl;
|
||||
const fp rad = static_cast<fp>(angle) * static_cast<fp>(kPi) / static_cast < fp>(180.f);
|
||||
logger() << "[script] npc_rotate_to: " << npcId << " angle " << angle << " degrees (" << static_cast<float>(rad) << " radians)" << std::endl;
|
||||
loc->gameServer().submit(SetCharacterFacingAngleCommand{
|
||||
loc->locationId(), npc->entityId(), rad, false});
|
||||
});
|
||||
|
||||
api.set_function("player_rotate_to",
|
||||
[loc](float angle) {
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
logger() << "[script] player_rotate_to: angle " << angle << " degrees (" << rad << " radians)" << std::endl;
|
||||
const fp rad = static_cast<fp>(angle) * static_cast<fp>(kPi) / static_cast <fp>(180.f);
|
||||
logger() << "[script] player_rotate_to: angle " << angle << " degrees (" << static_cast<float>(rad) << " radians)" << std::endl;
|
||||
loc->gameServer().submit(SetCharacterFacingAngleCommand{
|
||||
loc->locationId(), loc->player->entityId(), rad, false});
|
||||
});
|
||||
@@ -187,9 +191,9 @@ namespace FRG {
|
||||
}
|
||||
loc->gameServer().submit(SetCharacterHomePositionCommand{
|
||||
loc->locationId(), loc->player->entityId(),
|
||||
Eigen::Vector3f(x, 0.f, z)});
|
||||
Vec3fp(static_cast<fp>(x), static_cast<fp>(0.f), static_cast<fp>(z))});
|
||||
loc->setCharacterTarget(
|
||||
*loc->player, Eigen::Vector3f(x, y, z), std::move(cbName));
|
||||
*loc->player, Vec3fp(static_cast<fp>(x), static_cast<fp>(y), static_cast<fp>(z)), std::move(cbName));
|
||||
});
|
||||
|
||||
|
||||
@@ -423,12 +427,12 @@ namespace FRG {
|
||||
api.set_function("get_player_x",
|
||||
[loc]() -> float {
|
||||
auto state = loc->player ? loc->player->stateSnapshot() : nullptr;
|
||||
return state ? state->position.x() : 0.0f;
|
||||
return state ? static_cast<float>(state->position.x) : 0.0f;
|
||||
});
|
||||
api.set_function("get_player_z",
|
||||
[loc]() -> float {
|
||||
auto state = loc->player ? loc->player->stateSnapshot() : nullptr;
|
||||
return state ? state->position.z() : 0.0f;
|
||||
return state ? static_cast<float>(state->position.z) : 0.0f;
|
||||
});
|
||||
|
||||
api.set_function("refill_current_player_hp",
|
||||
@@ -592,7 +596,7 @@ namespace FRG {
|
||||
std::cerr << "[script] npc_set_position: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
Eigen::Vector3f pos(x, y, z);
|
||||
Vec3fp pos(static_cast<fp>(x), static_cast<fp>(y), static_cast<fp>(z));
|
||||
loc->gameServer().submit(SetCharacterPositionCommand{
|
||||
loc->locationId(), npc->entityId(), pos, true});
|
||||
loc->setCharacterTarget(*npc, pos);
|
||||
@@ -606,7 +610,8 @@ namespace FRG {
|
||||
std::cerr << "[script] npc_set_rotation: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
const fp rad = static_cast<fp>(angle) * static_cast<fp>(kPi) / static_cast <fp>(180.f);
|
||||
|
||||
loc->gameServer().submit(SetCharacterFacingAngleCommand{
|
||||
loc->locationId(), npc->entityId(), rad, true});
|
||||
});
|
||||
@@ -625,7 +630,7 @@ namespace FRG {
|
||||
|
||||
api.set_function("player_set_position",
|
||||
[loc](float x, float y, float z) {
|
||||
Eigen::Vector3f pos(x, y, z);
|
||||
Vec3fp pos(static_cast<fp>(x), static_cast<fp>(y), static_cast<fp>(z));
|
||||
loc->gameServer().submit(SetCharacterPositionCommand{
|
||||
loc->locationId(), loc->player->entityId(), pos, true});
|
||||
loc->setCharacterTarget(*loc->player, pos);
|
||||
@@ -633,7 +638,8 @@ namespace FRG {
|
||||
|
||||
api.set_function("player_set_rotation",
|
||||
[loc](float angle) {
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
const fp rad = static_cast<fp>(angle) * static_cast<fp>(kPi) / static_cast <fp>(180.f);
|
||||
|
||||
loc->gameServer().submit(SetCharacterFacingAngleCommand{
|
||||
loc->locationId(), loc->player->entityId(), rad, true});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <unordered_set>
|
||||
#include <math.h>
|
||||
|
||||
namespace FRG {
|
||||
namespace {
|
||||
@@ -172,7 +173,7 @@ CharacterState NpcData::toCharacterState() const
|
||||
CharacterState state;
|
||||
state.npcName = name;
|
||||
state.position = {positionX, positionY, positionZ};
|
||||
state.homePosition = {positionX, 0.f, positionZ};
|
||||
state.homePosition = {positionX, static_cast<fp>(0.f), positionZ};
|
||||
state.stopInPlace();
|
||||
state.facingAngle = facingAngle;
|
||||
state.walkSpeed = walkSpeed;
|
||||
@@ -183,9 +184,9 @@ CharacterState NpcData::toCharacterState() const
|
||||
state.visibleRange = visibleRange;
|
||||
state.canAttack = canAttack;
|
||||
state.enabled = enabled;
|
||||
state.collisionRadius = navigationAvoidanceRadius;
|
||||
state.movementContactRadius =
|
||||
(std::min)(movementContactRadius, navigationAvoidanceRadius);
|
||||
//state.collisionRadius = navigationAvoidanceRadius;
|
||||
//state.movementContactRadius =
|
||||
// (std::min)(movementContactRadius, navigationAvoidanceRadius);
|
||||
state.interactionRadius = interactionRadius;
|
||||
state.configureNavigationRetryCooldowns(
|
||||
id, navigationRetryCooldownTicks);
|
||||
@@ -293,7 +294,7 @@ LocationDefinition LocationContentLoader::load(
|
||||
for (const auto& item : root.value("npcs", json::array())) {
|
||||
NpcData data;
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty() || data.id == "player" ||
|
||||
if (data.id.empty() || data.id == "player1" ||
|
||||
!ids.insert(data.id).second) {
|
||||
throw std::runtime_error(
|
||||
"Missing, duplicate, or reserved NPC id in " +
|
||||
@@ -309,8 +310,8 @@ LocationDefinition LocationContentLoader::load(
|
||||
data.meshTextures[name] = path.get<std::string>();
|
||||
}
|
||||
}
|
||||
data.interactionRadius =
|
||||
item.value("interactionRadius", 0.f);
|
||||
data.interactionRadius = static_cast<fp>(
|
||||
item.value<double>("interactionRadius", 0.0));
|
||||
data.animationIdlePath =
|
||||
item.value("animationIdlePath", "");
|
||||
data.animationWalkPath =
|
||||
@@ -327,11 +328,11 @@ LocationDefinition LocationContentLoader::load(
|
||||
item.value("animationActionToDeathPath", "");
|
||||
data.animationDeathIdlePath =
|
||||
item.value("animationDeathIdlePath", "");
|
||||
data.positionX = item.value("positionX", 0.f);
|
||||
data.positionY = item.value("positionY", 0.f);
|
||||
data.positionZ = item.value("positionZ", 0.f);
|
||||
data.walkSpeed = item.value("walkSpeed", 1.5f);
|
||||
data.rotationSpeed = item.value("rotationSpeed", 8.f);
|
||||
data.positionX = static_cast<fp>(item.value<double>("positionX", 0.0));
|
||||
data.positionY = static_cast<fp>(item.value<double>("positionY", 0.0));
|
||||
data.positionZ = static_cast<fp>(item.value<double>("positionZ", 0.0));
|
||||
data.walkSpeed = static_cast<fp>(item.value<double>("walkSpeed", 1.5));
|
||||
data.rotationSpeed = static_cast<fp>(item.value<double>("rotationSpeed", 8.0));
|
||||
data.modelScale = item.value("modelScale", 0.01f);
|
||||
const float degrees = kPi / 180.f;
|
||||
data.modelCorrectionRotX =
|
||||
@@ -341,12 +342,13 @@ LocationDefinition LocationContentLoader::load(
|
||||
data.modelCorrectionRotZ =
|
||||
item.value("modelCorrectionRotZ", 0.f) * degrees;
|
||||
data.facingAngle =
|
||||
item.value("facingAngle", 0.f) * degrees;
|
||||
static_cast<fp>(item.value<double>("facingAngle", 0.0)) * static_cast<fp>(kPi) / static_cast<fp>(180.0);
|
||||
data.hp = item.value("hp", 100.f);
|
||||
data.visibleRange =
|
||||
item.value("visibleRange", VISIBLE_RANGE);
|
||||
static_cast<fp>(item.value<double>("visibleRange", static_cast<double>(VISIBLE_RANGE)));
|
||||
data.canAttack = item.value("canAttack", false);
|
||||
data.enabled = item.value("enabled", true);
|
||||
/*
|
||||
data.navigationAvoidanceRadius = item.value(
|
||||
"navigationAvoidanceRadius",
|
||||
item.value("collisionRadius", 0.45f));
|
||||
@@ -359,6 +361,7 @@ LocationDefinition LocationContentLoader::load(
|
||||
0.05f, (std::min)(
|
||||
data.movementContactRadius,
|
||||
data.navigationAvoidanceRadius));
|
||||
*/
|
||||
const char* cooldownKey =
|
||||
item.contains("navigationRetryCooldownTicks")
|
||||
? "navigationRetryCooldownTicks"
|
||||
|
||||
@@ -58,23 +58,23 @@ struct NpcData {
|
||||
std::string animationActionToStandPath;
|
||||
std::string animationActionToDeathPath;
|
||||
std::string animationDeathIdlePath;
|
||||
float positionX = 0.f;
|
||||
float positionY = 0.f;
|
||||
float positionZ = 0.f;
|
||||
float walkSpeed = 1.5f;
|
||||
float rotationSpeed = 8.f;
|
||||
fp positionX = static_cast<fp>(0.f);
|
||||
fp positionY = static_cast<fp>(0.f);
|
||||
fp positionZ = static_cast<fp>(0.f);
|
||||
fp walkSpeed = static_cast<fp>(1.5f);
|
||||
fp rotationSpeed = static_cast<fp>(8.f);
|
||||
float modelScale = 0.01f;
|
||||
float modelCorrectionRotX = 0.f;
|
||||
float modelCorrectionRotY = 0.f;
|
||||
float modelCorrectionRotZ = 0.f;
|
||||
float facingAngle = 0.f;
|
||||
fp facingAngle = static_cast<fp>(0.f);
|
||||
float hp = 100.f;
|
||||
float visibleRange = VISIBLE_RANGE;
|
||||
fp visibleRange = static_cast<fp>(VISIBLE_RANGE);
|
||||
bool canAttack = false;
|
||||
bool enabled = true;
|
||||
float navigationAvoidanceRadius = 0.45f;
|
||||
float movementContactRadius = 0.32f;
|
||||
float interactionRadius = 0.f;
|
||||
//fp navigationAvoidanceRadius = static_cast<fp>(0.45f);
|
||||
//fp movementContactRadius = static_cast<fp>(0.32f);
|
||||
fp interactionRadius = static_cast<fp>(0.f);
|
||||
std::vector<int> navigationRetryCooldownTicks;
|
||||
|
||||
CharacterState toCharacterState() const;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
#include "../navigation/PathFinder.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
@@ -110,7 +111,7 @@ struct SetNavigationAreaAvailableCommand {
|
||||
struct AddNavigationObstacleCommand {
|
||||
std::string locationId;
|
||||
std::string name;
|
||||
std::vector<Eigen::Vector2f> polygon;
|
||||
std::vector<Vec2fp> polygon;
|
||||
};
|
||||
struct AddInventoryItemCommand { std::string itemId; };
|
||||
struct RemoveInventoryItemCommand { std::string itemId; };
|
||||
@@ -136,19 +137,19 @@ struct SetGlobalFloatCommand { std::string key; float value; };
|
||||
struct SetCharacterPositionCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
Eigen::Vector3f position;
|
||||
Vec3fp position;
|
||||
bool updateHomePosition = false;
|
||||
};
|
||||
struct SetCharacterFacingAngleCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
float angle;
|
||||
fp angle;
|
||||
bool setCurrentAngle = false;
|
||||
};
|
||||
struct SetCharacterHomePositionCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
Eigen::Vector3f position;
|
||||
Vec3fp position;
|
||||
};
|
||||
struct SetCharacterTargetCommand {
|
||||
std::string locationId;
|
||||
@@ -172,7 +173,7 @@ struct ClearCharacterAttackTargetCommand {
|
||||
struct SetCharacterDestinationCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
Eigen::Vector3f target;
|
||||
Vec3fp target;
|
||||
std::string callbackName;
|
||||
};
|
||||
struct StopCharacterCommand {
|
||||
|
||||
@@ -70,11 +70,11 @@ void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs)
|
||||
battleZoneTestBotElapsedMs_ %= kBattleZoneTestBotIntervalMs;
|
||||
|
||||
static std::mt19937 randomEngine(std::random_device{}());
|
||||
static std::uniform_real_distribution<float> coordinate(-10.0f, 10.0f);
|
||||
static std::uniform_int_distribution<int> coordinate(-100, 100); //Divide by 10 then.
|
||||
submit(SetCharacterDestinationCommand{
|
||||
state_.currentLocationName,
|
||||
kBattleZonePlayer2Id,
|
||||
Eigen::Vector3f(coordinate(randomEngine), 0.f, coordinate(randomEngine)),
|
||||
Vec3fp(static_cast<fp>(coordinate(randomEngine)) / static_cast<fp>(10), static_cast<fp>(0.f), coordinate(randomEngine) / static_cast<fp>(10)),
|
||||
std::string()});
|
||||
}
|
||||
|
||||
@@ -520,8 +520,7 @@ void InternalGameServer::update(std::int64_t deltaMs)
|
||||
if (!event.events.navigationRequested) continue;
|
||||
if (event.events.forceNavigationReplan)
|
||||
stored->second.replanCharacter(
|
||||
event.characterId, state_.players,
|
||||
event.events.blockingCharacterId);
|
||||
event.characterId, state_.players);
|
||||
else
|
||||
stored->second.setCharacterDestination(
|
||||
event.characterId, event.events.navigationTarget, {},
|
||||
|
||||
@@ -91,6 +91,44 @@ bool readTarget(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readTargetFp(
|
||||
const nlohmann::json& value, FRG::Vec3fp& target, std::string& error)
|
||||
{
|
||||
if (!value.is_array() || value.size() != 3) {
|
||||
error = "target must contain three coordinates";
|
||||
return false;
|
||||
}
|
||||
|
||||
FRG::fp coords[3];
|
||||
for (std::size_t index = 0; index < 3; ++index) {
|
||||
if (!value[index].is_number()) {
|
||||
error = "target coordinates must be numbers";
|
||||
return false;
|
||||
}
|
||||
const double coordinate = value[index].get<double>();
|
||||
if (!std::isfinite(coordinate) || std::abs(coordinate) > 100000.0) {
|
||||
error = "target coordinate is not finite or is out of range";
|
||||
return false;
|
||||
}
|
||||
// Округляем до сотых в double и конвертируем в Q20.12 (fp)
|
||||
coords[index] = FRG::fp(coordinate);
|
||||
}
|
||||
|
||||
target.x = coords[0];
|
||||
target.y = coords[1];
|
||||
target.z = coords[2];
|
||||
return true;
|
||||
}
|
||||
|
||||
nlohmann::json targetJsonFp(const FRG::Vec3fp& target)
|
||||
{
|
||||
return nlohmann::json::array({
|
||||
static_cast<double>(target.x),
|
||||
static_cast<double>(target.y),
|
||||
static_cast<double>(target.z)
|
||||
});
|
||||
}
|
||||
|
||||
nlohmann::json targetJson(const Eigen::Vector3f& target)
|
||||
{
|
||||
return nlohmann::json::array({target.x(), target.y(), target.z()});
|
||||
@@ -161,7 +199,7 @@ bool parseInitialStateMessage(
|
||||
}
|
||||
|
||||
std::uint64_t LockstepTimeline::schedule(
|
||||
const std::string& playerId, const Eigen::Vector3f& target)
|
||||
const std::string& playerId, const Vec3fp& target)
|
||||
{
|
||||
const std::uint64_t sequence = nextCommandSequence_++;
|
||||
pending_[currentTick_ + inputDelayTicks_].push_back(
|
||||
@@ -206,7 +244,7 @@ std::string makeDestinationInputMessage(const DestinationInputMessage& message)
|
||||
{
|
||||
return envelope("destination_input", {
|
||||
{"clientSequence", message.clientSequence},
|
||||
{"target", targetJson(message.target)}
|
||||
{"target", targetJsonFp(message.target)}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,7 +257,7 @@ bool parseDestinationInputMessage(
|
||||
return false;
|
||||
if (!payload.contains("target"))
|
||||
return error = "target is required", false;
|
||||
return readTarget(payload["target"], message.target, error);
|
||||
return readTargetFp(payload["target"], message.target, error);
|
||||
}
|
||||
|
||||
std::string makeCharacterActionInputMessage(
|
||||
@@ -265,7 +303,7 @@ std::string makeTickMessage(const TickMessage& message)
|
||||
{"type", "destination"},
|
||||
{"commandSequence", command.commandSequence},
|
||||
{"playerId", command.playerId},
|
||||
{"target", targetJson(command.target)}
|
||||
{"target", targetJsonFp(command.target)}
|
||||
});
|
||||
} else {
|
||||
if constexpr (std::is_same_v<T, ScheduledCharacterEnabledCommand>) {
|
||||
@@ -320,7 +358,7 @@ bool parseTickMessage(
|
||||
destination.commandSequence = previousSequence;
|
||||
if (!value.contains("playerId") || !value["playerId"].is_string() ||
|
||||
!value.contains("target") ||
|
||||
!readTarget(value["target"], destination.target, error)) {
|
||||
!readTargetFp(value["target"], destination.target, error)) {
|
||||
if (error.empty()) error = "invalid destination command";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
#include "../navigation/PathFinder.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
@@ -25,7 +26,7 @@ struct InitialStateMessage {
|
||||
|
||||
struct DestinationInputMessage {
|
||||
std::uint64_t clientSequence = 0;
|
||||
Eigen::Vector3f target = Eigen::Vector3f::Zero();
|
||||
Vec3fp target;
|
||||
};
|
||||
|
||||
struct CharacterActionInputMessage {
|
||||
@@ -37,7 +38,7 @@ struct CharacterActionInputMessage {
|
||||
struct ScheduledDestinationCommand {
|
||||
std::uint64_t commandSequence = 0;
|
||||
std::string playerId;
|
||||
Eigen::Vector3f target = Eigen::Vector3f::Zero();
|
||||
Vec3fp target;
|
||||
};
|
||||
|
||||
struct ScheduledCharacterEnabledCommand {
|
||||
@@ -73,7 +74,7 @@ public:
|
||||
|
||||
std::uint64_t currentTick() const { return currentTick_; }
|
||||
std::uint64_t schedule(
|
||||
const std::string& playerId, const Eigen::Vector3f& target);
|
||||
const std::string& playerId, const Vec3fp& target);
|
||||
std::uint64_t scheduleCharacterEnabled(
|
||||
const std::string& locationId, const std::string& characterId,
|
||||
bool enabled);
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
|
||||
namespace FRG {
|
||||
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
@@ -341,7 +344,7 @@ namespace FRG {
|
||||
data.meshTextures[it.key()] = it.value().get<std::string>();
|
||||
}
|
||||
}
|
||||
data.interactionRadius = item.value("interactionRadius", 0.0f);
|
||||
data.interactionRadius = static_cast<fp>(item.value<double>("interactionRadius", 0.0));
|
||||
data.animationIdlePath = item.value("animationIdlePath", "");
|
||||
data.animationWalkPath = item.value("animationWalkPath", "");
|
||||
data.animationActionIdlePath = item.value("animationActionIdlePath", "");
|
||||
@@ -350,18 +353,18 @@ namespace FRG {
|
||||
data.animationActionToStandPath = item.value("animationActionToStandPath", "");
|
||||
data.animationActionToDeathPath = item.value("animationActionToDeathPath", "");
|
||||
data.animationDeathIdlePath = item.value("animationDeathIdlePath", "");
|
||||
data.positionX = item.value("positionX", 0.0f);
|
||||
data.positionY = item.value("positionY", 0.0f);
|
||||
data.positionZ = item.value("positionZ", 0.0f);
|
||||
data.walkSpeed = item.value("walkSpeed", 1.5f);
|
||||
data.rotationSpeed = item.value("rotationSpeed", 8.0f);
|
||||
data.positionX = static_cast<fp>(item.value<double>("positionX", 0.0));
|
||||
data.positionY = static_cast<fp>(item.value<double>("positionY", 0.0));
|
||||
data.positionZ = static_cast<fp>(item.value<double>("positionZ", 0.0));
|
||||
data.walkSpeed = static_cast<fp>(item.value<double>("walkSpeed", 1.5));
|
||||
data.rotationSpeed = static_cast<fp>(item.value<double>("rotationSpeed", 8.0));
|
||||
data.modelScale = item.value("modelScale", 0.01f);
|
||||
data.modelCorrectionRotX = item.value("modelCorrectionRotX", 0.0f) * static_cast<float>(M_PI) / 180.0f;
|
||||
data.modelCorrectionRotY = item.value("modelCorrectionRotY", 0.0f) * static_cast<float>(M_PI) / 180.0f;
|
||||
data.modelCorrectionRotZ = item.value("modelCorrectionRotZ", 0.0f) * static_cast<float>(M_PI) / 180.0f;
|
||||
data.facingAngle = item.value("facingAngle", 0.0f) * static_cast<float>(M_PI) / 180.0f;
|
||||
data.facingAngle = static_cast<fp>(item.value("facingAngle", 0.0f)) * static_cast<fp>(kPi) / static_cast<fp>(180.0f);
|
||||
data.hp = item.value("hp", 100.0f);
|
||||
data.visibleRange = item.value("visibleRange", VISIBLE_RANGE);
|
||||
data.visibleRange = static_cast<fp>(item.value<double>("visibleRange", static_cast<double>(VISIBLE_RANGE)));
|
||||
data.canAttack = item.value("canAttack", false);
|
||||
data.enabled = item.value("enabled", true);
|
||||
|
||||
@@ -401,7 +404,7 @@ namespace FRG {
|
||||
renderer, zipPath.c_str());
|
||||
if (npc) {
|
||||
logger() << "Successfully loaded NPC: " << npcData.name << " at ("
|
||||
<< npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl;
|
||||
<< static_cast<float>(npcData.positionX) << ", " << static_cast<float>(npcData.positionY) << ", " << static_cast<float>(npcData.positionZ) << ")" << std::endl;
|
||||
npcs.push_back(std::move(npc));
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
|
||||
+28
-13
@@ -2,6 +2,7 @@
|
||||
#include "Environment.h"
|
||||
#include "utils/Utils.h"
|
||||
#include <iostream>
|
||||
#include <cmath>
|
||||
#ifdef __ANDROID__
|
||||
#include <android/log.h>
|
||||
#endif
|
||||
@@ -75,14 +76,27 @@ extern "C" void EMSCRIPTEN_KEEPALIVE onFileSystemLoaded() {
|
||||
}
|
||||
|
||||
|
||||
// Single definition of the "logical" viewport size: the *visible* area of the page
|
||||
// (visualViewport), not 100vh, which on mobile includes the area under the URL bar.
|
||||
static void getViewportCssSize(int& w, int& h) {
|
||||
w = EM_ASM_INT({
|
||||
var vv = window.visualViewport;
|
||||
return Math.round((vv && vv.width) || window.innerWidth);
|
||||
});
|
||||
h = EM_ASM_INT({
|
||||
var vv = window.visualViewport;
|
||||
return Math.round((vv && vv.height) || window.innerHeight);
|
||||
});
|
||||
}
|
||||
|
||||
static void applyResize(int logicalW, int logicalH) {
|
||||
// Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung)
|
||||
double dpr = emscripten_get_device_pixel_ratio();
|
||||
//double dpr = 1; // low quality
|
||||
|
||||
// Вычисляем реальные физические пиксели
|
||||
int physicalW = static_cast<int>(logicalW * dpr);
|
||||
int physicalH = static_cast<int>(logicalH * dpr);
|
||||
int physicalW = static_cast<int>(std::lround(logicalW * dpr));
|
||||
int physicalH = static_cast<int>(std::lround(logicalH * dpr));
|
||||
|
||||
// Устанавливаем размер внутреннего буфера канваса
|
||||
emscripten_set_canvas_element_size("#canvas", physicalW, physicalH);
|
||||
@@ -94,6 +108,11 @@ static void applyResize(int logicalW, int logicalH) {
|
||||
SDL_SetWindowSize(FRG::Environment::window, physicalW, physicalH);
|
||||
}
|
||||
|
||||
// SDL may have written an inline CSS size derived from the physical pixels, which
|
||||
// overrides the stylesheet. Re-assert the CSS box so its aspect always matches the
|
||||
// drawing buffer (otherwise the image is stretched / cropped on mobile).
|
||||
emscripten_set_element_css_size("#canvas", logicalW, logicalH);
|
||||
|
||||
// Пушим событие, чтобы движок пересчитал матрицы проекции
|
||||
SDL_Event e = {};
|
||||
e.type = SDL_WINDOWEVENT;
|
||||
@@ -118,8 +137,8 @@ void MainLoop() {
|
||||
|
||||
// Теперь DOM перестроен, можно забирать актуальные размеры.
|
||||
// Если canvas должен занимать все окно браузера:
|
||||
int canvasW = EM_ASM_INT({ return window.innerWidth; });
|
||||
int canvasH = EM_ASM_INT({ return window.innerHeight; });
|
||||
int canvasW, canvasH;
|
||||
getViewportCssSize(canvasW, canvasH);
|
||||
|
||||
applyResize(canvasW, canvasH);
|
||||
}
|
||||
@@ -141,14 +160,10 @@ EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void
|
||||
|
||||
|
||||
EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) {
|
||||
double w, h;
|
||||
emscripten_get_element_css_size("window", &w, &h);
|
||||
int w, h;
|
||||
getViewportCssSize(w, h);
|
||||
|
||||
// Если "window" не поддерживается вашей версией SDK, используем данные из e:
|
||||
// int logicalW = e->windowInnerWidth;
|
||||
// int logicalH = e->windowInnerHeight;
|
||||
|
||||
applyResize(static_cast<int>(w), static_cast<int>(h));
|
||||
applyResize(w, h);
|
||||
return EM_FALSE;
|
||||
}
|
||||
|
||||
@@ -181,8 +196,8 @@ int main(int argc, char* argv[]) {
|
||||
emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized);
|
||||
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onFullscreenChanged);
|
||||
|
||||
int canvasW = EM_ASM_INT({ return window.innerWidth; });
|
||||
int canvasH = EM_ASM_INT({ return window.innerHeight; });
|
||||
int canvasW, canvasH;
|
||||
getViewportCssSize(canvasW, canvasH);
|
||||
applyResize(canvasW, canvasH);
|
||||
|
||||
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
|
||||
|
||||
@@ -12,8 +12,27 @@
|
||||
namespace FRG {
|
||||
|
||||
using json = nlohmann::json;
|
||||
Vec3fp operator-(Vec3fp a, Vec3fp b)
|
||||
{
|
||||
return Vec3fp{
|
||||
a.x - b.x,
|
||||
a.y - b.y,
|
||||
a.z - b.z
|
||||
};
|
||||
}
|
||||
Vec3fp operator+(Vec3fp a, Vec3fp b)
|
||||
{
|
||||
return Vec3fp{
|
||||
a.x + b.x,
|
||||
a.y + b.y,
|
||||
a.z + b.z
|
||||
};
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
|
||||
|
||||
std::vector<Eigen::Vector2f> readPolygon(const json& item)
|
||||
{
|
||||
std::vector<Eigen::Vector2f> polygon;
|
||||
@@ -79,7 +98,7 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
void PathFinder::build(const std::string& configPath,
|
||||
void PathFinderOld::build(const std::string& configPath,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
ready = false;
|
||||
@@ -115,22 +134,23 @@ void PathFinder::build(const std::string& configPath,
|
||||
<< ", cell=" << cellSize << ", areas=" << areas.size() << "\n";
|
||||
}
|
||||
|
||||
std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
||||
std::vector<Eigen::Vector3f> PathFinderOld::findPath(const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end) const
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(*queryMutex);
|
||||
return runSearch(start, end, 0, false);
|
||||
//const std::lock_guard<std::mutex> lock(*queryMutex);
|
||||
//return runSearch(start, end, 0, false);
|
||||
return std::vector<Eigen::Vector3f>{start, end};
|
||||
}
|
||||
|
||||
std::vector<Eigen::Vector3f> PathFinder::findPath(const Eigen::Vector3f& start,
|
||||
/*
|
||||
std::vector<Eigen::Vector3f> PathFinderOld::findPath(const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end,
|
||||
const std::vector<DynamicObstacle>& dynamicObstacles) const
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(*queryMutex);
|
||||
return runSearch(start, end, buildDynamicOverlay(dynamicObstacles), false);
|
||||
}
|
||||
|
||||
std::uint32_t PathFinder::buildDynamicOverlay(
|
||||
}*/
|
||||
std::uint32_t PathFinderOld::buildDynamicOverlay(
|
||||
const std::vector<DynamicObstacle>& dynamicObstacles) const
|
||||
{
|
||||
const size_t cellCount = walkable.size();
|
||||
@@ -172,7 +192,7 @@ std::uint32_t PathFinder::buildDynamicOverlay(
|
||||
return dynamicGeneration;
|
||||
}
|
||||
|
||||
bool PathFinder::isCellWalkableOverlay(
|
||||
bool PathFinderOld::isCellWalkableOverlay(
|
||||
const Cell& cell, std::uint32_t generation) const
|
||||
{
|
||||
if (!isCellWalkable(cell)) return false;
|
||||
@@ -180,7 +200,7 @@ bool PathFinder::isCellWalkableOverlay(
|
||||
dynamicBlockedGeneration[static_cast<size_t>(indexOf(cell))] != generation;
|
||||
}
|
||||
|
||||
bool PathFinder::findNearestWalkableCellOverlay(
|
||||
bool PathFinderOld::findNearestWalkableCellOverlay(
|
||||
const Eigen::Vector3f& point, Cell& out, std::uint32_t generation) const
|
||||
{
|
||||
Cell origin;
|
||||
@@ -212,7 +232,7 @@ bool PathFinder::findNearestWalkableCellOverlay(
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PathFinder::hasLineOfSightOverlay(
|
||||
bool PathFinderOld::hasLineOfSightOverlay(
|
||||
const Cell& from, const Cell& to, std::uint32_t generation) const
|
||||
{
|
||||
int x = from.x;
|
||||
@@ -244,7 +264,7 @@ bool PathFinder::hasLineOfSightOverlay(
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<PathFinder::Cell> PathFinder::smoothCellsOverlay(
|
||||
std::vector<PathFinderOld::Cell> PathFinderOld::smoothCellsOverlay(
|
||||
const std::vector<Cell>& cells, std::uint32_t generation) const
|
||||
{
|
||||
if (cells.size() <= 2) return cells;
|
||||
@@ -263,7 +283,7 @@ std::vector<PathFinder::Cell> PathFinder::smoothCellsOverlay(
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Eigen::Vector3f> PathFinder::runSearch(
|
||||
std::vector<Eigen::Vector3f> PathFinderOld::runSearch(
|
||||
const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end,
|
||||
std::uint32_t obstacleGeneration,
|
||||
@@ -385,23 +405,25 @@ std::vector<Eigen::Vector3f> PathFinder::runSearch(
|
||||
return path;
|
||||
}
|
||||
|
||||
std::vector<Eigen::Vector3f> PathFinder::findPathToNearest(
|
||||
std::vector<Eigen::Vector3f> PathFinderOld::findPathToNearest(
|
||||
const Eigen::Vector3f& start, const Eigen::Vector3f& end) const
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(*queryMutex);
|
||||
return runSearch(start, end, 0, true);
|
||||
//const std::lock_guard<std::mutex> lock(*queryMutex);
|
||||
// return runSearch(start, end, 0, true);
|
||||
return std::vector<Eigen::Vector3f>{start, end};
|
||||
}
|
||||
|
||||
std::vector<Eigen::Vector3f> PathFinder::findPathToNearest(
|
||||
/*
|
||||
std::vector<Eigen::Vector3f> PathFinderOld::findPathToNearest(
|
||||
const Eigen::Vector3f& start, const Eigen::Vector3f& end,
|
||||
const std::vector<DynamicObstacle>& dynamicObstacles) const
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(*queryMutex);
|
||||
return runSearch(
|
||||
start, end, buildDynamicOverlay(dynamicObstacles), true);
|
||||
}
|
||||
}*/
|
||||
|
||||
bool PathFinder::setAreaAvailable(const std::string& areaName, bool available)
|
||||
bool PathFinderOld::setAreaAvailable(const std::string& areaName, bool available)
|
||||
{
|
||||
bool found = false;
|
||||
bool changed = false;
|
||||
@@ -420,13 +442,13 @@ bool PathFinder::setAreaAvailable(const std::string& areaName, bool available)
|
||||
return found;
|
||||
}
|
||||
|
||||
bool PathFinder::isWalkable(const Eigen::Vector3f& point) const
|
||||
bool PathFinderOld::isWalkable(const Eigen::Vector3f& point) const
|
||||
{
|
||||
Cell cell;
|
||||
return worldToCell(point, cell) && isCellWalkable(cell, walkable);
|
||||
}
|
||||
|
||||
void PathFinder::addObstaclePolygon(const ObstaclePolygon& polygon)
|
||||
void PathFinderOld::addObstaclePolygon(const ObstaclePolygon& polygon)
|
||||
{
|
||||
obstaclePolygons.push_back(polygon);
|
||||
if (ready) {
|
||||
@@ -434,7 +456,7 @@ void PathFinder::addObstaclePolygon(const ObstaclePolygon& polygon)
|
||||
}
|
||||
}
|
||||
|
||||
bool PathFinder::saveConfig(const std::string& path) const
|
||||
bool PathFinderOld::saveConfig(const std::string& path) const
|
||||
{
|
||||
json root;
|
||||
root["cellSize"] = cellSize;
|
||||
@@ -481,7 +503,7 @@ bool PathFinder::saveConfig(const std::string& path) const
|
||||
return saveJsonToFile(root, path);
|
||||
}
|
||||
|
||||
bool PathFinder::saveGrid(const std::string& path) const
|
||||
bool PathFinderOld::saveGrid(const std::string& path) const
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
if (!ready) return false;
|
||||
@@ -510,7 +532,7 @@ bool PathFinder::saveGrid(const std::string& path) const
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PathFinder::loadGrid(const std::string& path, const std::string& zipPath)
|
||||
bool PathFinderOld::loadGrid(const std::string& path, const std::string& zipPath)
|
||||
{
|
||||
std::string content;
|
||||
try {
|
||||
@@ -565,7 +587,7 @@ bool PathFinder::loadGrid(const std::string& path, const std::string& zipPath)
|
||||
return true;
|
||||
}
|
||||
|
||||
void PathFinder::loadConfig(const std::string& configPath, const std::string& zipPath)
|
||||
void PathFinderOld::loadConfig(const std::string& configPath, const std::string& zipPath)
|
||||
{
|
||||
cellSize = 0.4f;
|
||||
agentRadius = 0.45f;
|
||||
@@ -640,7 +662,7 @@ void PathFinder::loadConfig(const std::string& configPath, const std::string& zi
|
||||
boundaryPadding = (std::max)(boundaryPadding, 0.0f);
|
||||
}
|
||||
|
||||
void PathFinder::resetGridBounds()
|
||||
void PathFinderOld::resetGridBounds()
|
||||
{
|
||||
if (areas.empty()) {
|
||||
gridWidth = 0;
|
||||
@@ -682,14 +704,14 @@ void PathFinder::resetGridBounds()
|
||||
gridDepth = static_cast<int>(std::ceil((maxZ - minZ) / cellSize));
|
||||
}
|
||||
|
||||
void PathFinder::rebuildWalkableGrid()
|
||||
void PathFinderOld::rebuildWalkableGrid()
|
||||
{
|
||||
walkable.assign(static_cast<size_t>(gridWidth * gridDepth), 0);
|
||||
markAvailableAreasWalkable();
|
||||
markObstaclePolygonsBlocked();
|
||||
}
|
||||
|
||||
void PathFinder::markAvailableAreasWalkable()
|
||||
void PathFinderOld::markAvailableAreasWalkable()
|
||||
{
|
||||
const auto insideAvailableArea = [this](const Eigen::Vector2f& point) {
|
||||
for (const NavigationArea& area : areas) {
|
||||
@@ -763,7 +785,7 @@ void PathFinder::markAvailableAreasWalkable()
|
||||
}
|
||||
}
|
||||
|
||||
void PathFinder::markObstaclePolygonsBlocked()
|
||||
void PathFinderOld::markObstaclePolygonsBlocked()
|
||||
{
|
||||
if (obstaclePolygons.empty()) {
|
||||
return;
|
||||
@@ -800,7 +822,7 @@ void PathFinder::markObstaclePolygonsBlocked()
|
||||
}
|
||||
}
|
||||
|
||||
bool PathFinder::worldToCell(const Eigen::Vector3f& point, Cell& out) const
|
||||
bool PathFinderOld::worldToCell(const Eigen::Vector3f& point, Cell& out) const
|
||||
{
|
||||
if (gridWidth <= 0 || gridDepth <= 0) {
|
||||
out = {};
|
||||
@@ -812,7 +834,7 @@ bool PathFinder::worldToCell(const Eigen::Vector3f& point, Cell& out) const
|
||||
return isInsideGrid(out);
|
||||
}
|
||||
|
||||
Eigen::Vector3f PathFinder::cellCenter(const Cell& cell) const
|
||||
Eigen::Vector3f PathFinderOld::cellCenter(const Cell& cell) const
|
||||
{
|
||||
return {
|
||||
minX + (static_cast<float>(cell.x) + 0.5f) * cellSize,
|
||||
@@ -821,17 +843,17 @@ Eigen::Vector3f PathFinder::cellCenter(const Cell& cell) const
|
||||
};
|
||||
}
|
||||
|
||||
bool PathFinder::isInsideGrid(const Cell& cell) const
|
||||
bool PathFinderOld::isInsideGrid(const Cell& cell) const
|
||||
{
|
||||
return cell.x >= 0 && cell.z >= 0 && cell.x < gridWidth && cell.z < gridDepth;
|
||||
}
|
||||
|
||||
bool PathFinder::isCellWalkable(const Cell& cell) const
|
||||
bool PathFinderOld::isCellWalkable(const Cell& cell) const
|
||||
{
|
||||
return isCellWalkable(cell, walkable);
|
||||
}
|
||||
|
||||
bool PathFinder::isCellWalkable(const Cell& cell, const std::vector<unsigned char>& walkableGrid) const
|
||||
bool PathFinderOld::isCellWalkable(const Cell& cell, const std::vector<unsigned char>& walkableGrid) const
|
||||
{
|
||||
const int idx = indexOf(cell);
|
||||
return isInsideGrid(cell) &&
|
||||
@@ -840,17 +862,17 @@ bool PathFinder::isCellWalkable(const Cell& cell, const std::vector<unsigned cha
|
||||
walkableGrid[static_cast<size_t>(idx)] != 0;
|
||||
}
|
||||
|
||||
int PathFinder::indexOf(const Cell& cell) const
|
||||
int PathFinderOld::indexOf(const Cell& cell) const
|
||||
{
|
||||
return cell.z * gridWidth + cell.x;
|
||||
}
|
||||
|
||||
bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out) const
|
||||
bool PathFinderOld::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out) const
|
||||
{
|
||||
return findNearestWalkableCell(point, out, walkable);
|
||||
}
|
||||
|
||||
bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out, const std::vector<unsigned char>& walkableGrid) const
|
||||
bool PathFinderOld::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out, const std::vector<unsigned char>& walkableGrid) const
|
||||
{
|
||||
Cell origin;
|
||||
if (!worldToCell(point, origin)) {
|
||||
@@ -885,12 +907,12 @@ bool PathFinder::findNearestWalkableCell(const Eigen::Vector3f& point, Cell& out
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to) const
|
||||
bool PathFinderOld::hasLineOfSight(const Cell& from, const Cell& to) const
|
||||
{
|
||||
return hasLineOfSight(from, to, walkable);
|
||||
}
|
||||
|
||||
bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to, const std::vector<unsigned char>& walkableGrid) const
|
||||
bool PathFinderOld::hasLineOfSight(const Cell& from, const Cell& to, const std::vector<unsigned char>& walkableGrid) const
|
||||
{
|
||||
const Eigen::Vector3f a = cellCenter(from);
|
||||
const Eigen::Vector3f b = cellCenter(to);
|
||||
@@ -909,12 +931,12 @@ bool PathFinder::hasLineOfSight(const Cell& from, const Cell& to, const std::vec
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& cells) const
|
||||
std::vector<PathFinderOld::Cell> PathFinderOld::smoothCells(const std::vector<Cell>& cells) const
|
||||
{
|
||||
return smoothCells(cells, walkable);
|
||||
}
|
||||
|
||||
std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& cells, const std::vector<unsigned char>& walkableGrid) const
|
||||
std::vector<PathFinderOld::Cell> PathFinderOld::smoothCells(const std::vector<Cell>& cells, const std::vector<unsigned char>& walkableGrid) const
|
||||
{
|
||||
if (cells.size() <= 2) {
|
||||
return cells;
|
||||
@@ -937,7 +959,7 @@ std::vector<PathFinder::Cell> PathFinder::smoothCells(const std::vector<Cell>& c
|
||||
return result;
|
||||
}
|
||||
|
||||
bool PathFinder::pointInPolygon(float x, float z, const std::vector<Eigen::Vector2f>& polygon)
|
||||
bool PathFinderOld::pointInPolygon(float x, float z, const std::vector<Eigen::Vector2f>& polygon)
|
||||
{
|
||||
bool inside = false;
|
||||
size_t previous = polygon.size() - 1;
|
||||
|
||||
+577
-5
@@ -1,15 +1,587 @@
|
||||
#pragma once
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#ifdef min
|
||||
#undef min
|
||||
#endif
|
||||
#ifdef max
|
||||
#undef max
|
||||
#endif
|
||||
#include "../../thirdparty/fpm-1.1.0/include/fpm/fixed.hpp"
|
||||
#include "../../thirdparty/fpm-1.1.0/include/fpm/math.hpp"
|
||||
|
||||
// Сторонние библиотеки для геометрии (Header-only / легко подключаемые):
|
||||
// 1. Clipper2: https://github.com/PolyClipping/Clipper2/releases/tag/Clipper2_2.0.1
|
||||
// 2. CDT (Constrained Delaunay Triangulation): https://github.com/artem-ogre/CDT
|
||||
|
||||
#include "../../thirdparty/Clipper2-Clipper2_2.0.1/CPP/build/Release/include/clipper2/clipper.h"
|
||||
#include "../../thirdparty/CDT-2.0.2/CDT/build/release/include/CDT.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class PathFinder {
|
||||
|
||||
//using fp = fpm::fixed<std::int32_t, std::int64_t, 16>;
|
||||
inline constexpr unsigned int FP_FRACTION_BITS = 12;
|
||||
using fp = fpm::fixed<std::int32_t, std::int64_t, FP_FRACTION_BITS>;
|
||||
|
||||
struct Vec2fp {
|
||||
fp x{ 0 };
|
||||
fp y{ 0 };
|
||||
|
||||
Vec2fp() = default;
|
||||
Vec2fp(fp _x, fp _y) : x(_x), y(_y) {}
|
||||
|
||||
// Конвертация из float только на этапе загрузки/ввода
|
||||
static Vec2fp fromFloat(float fx, float fy) {
|
||||
return Vec2fp(fp(fx), fp(fy));
|
||||
}
|
||||
|
||||
Vec2fp operator+(const Vec2fp& o) const { return { x + o.x, y + o.y }; }
|
||||
Vec2fp operator-(const Vec2fp& o) const { return { x - o.x, y - o.y }; }
|
||||
Vec2fp operator*(fp s) const { return { x * s, y * s }; }
|
||||
Vec2fp operator/(fp s) const { return { x / s, y / s }; }
|
||||
bool operator==(const Vec2fp& o) const { return x == o.x && y == o.y; }
|
||||
bool operator!=(const Vec2fp& o) const { return !(*this == o); }
|
||||
fp dot(const Vec2fp& o) const { return x * o.x + y * o.y; }
|
||||
|
||||
// 2D Векторное произведение (Z-компонента): определяет, с какой стороны лежит точка
|
||||
fp cross(const Vec2fp& o) const { return x * o.y - y * o.x; }
|
||||
|
||||
fp lengthSq() const { return x * x + y * y; }
|
||||
fp length() const { return fpm::sqrt(lengthSq()); }
|
||||
|
||||
Eigen::Vector2f toEigen() const {
|
||||
return Eigen::Vector2f(static_cast<float>(x), static_cast<float>(y));
|
||||
}
|
||||
|
||||
static Vec2fp fromEigen(const Eigen::Vector2f& v) {
|
||||
return Vec2fp(fp(v.x()), fp(v.y()));
|
||||
}
|
||||
};
|
||||
|
||||
struct Vec3fp {
|
||||
fp x{ 0 }, y{ 0 }, z{ 0 };
|
||||
Vec3fp() = default;
|
||||
Vec3fp(fp _x, fp _y, fp _z) : x(_x), y(_y), z(_z) {}
|
||||
|
||||
static Vec3fp fromEigen(const Eigen::Vector3f& v) {
|
||||
return { fp(v.x()), fp(v.y()), fp(v.z()) };
|
||||
}
|
||||
Eigen::Vector3f toEigen() const {
|
||||
return Eigen::Vector3f(static_cast<float>(x), static_cast<float>(y), static_cast<float>(z));
|
||||
}
|
||||
|
||||
fp normSquared() const
|
||||
{
|
||||
return x * x + y * y + z * z;
|
||||
}
|
||||
Vec3fp operator/(fp value) const
|
||||
{
|
||||
if (value == static_cast<fp>(0.0))
|
||||
{
|
||||
throw std::runtime_error("Vec3fp error: division by zero");
|
||||
return Vec3fp();
|
||||
}
|
||||
Vec3fp r;
|
||||
r.x = x / value;
|
||||
r.y = y / value;
|
||||
r.z = z / value;
|
||||
return r;
|
||||
}
|
||||
Vec3fp operator*(const fp value) const
|
||||
{
|
||||
Vec3fp r;
|
||||
r.x = x * value;
|
||||
r.y = y * value;
|
||||
r.z = z * value;
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
Vec3fp operator-(Vec3fp a, Vec3fp b);
|
||||
Vec3fp operator+(Vec3fp a, Vec3fp b);
|
||||
|
||||
inline std::vector<Eigen::Vector2f> toEigenVector(const std::vector<FRG::Vec2fp>& src) {
|
||||
std::vector<Eigen::Vector2f> dst;
|
||||
dst.reserve(src.size());
|
||||
for (const auto& pt : src) {
|
||||
dst.emplace_back(static_cast<float>(pt.x), static_cast<float>(pt.y));
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
inline std::vector<FRG::Vec2fp> fromEigenVector(const std::vector<Eigen::Vector2f>& src) {
|
||||
std::vector<FRG::Vec2fp> dst;
|
||||
dst.reserve(src.size());
|
||||
for (const auto& pt : src) {
|
||||
dst.emplace_back(FRG::fp(pt.x()), FRG::fp(pt.y()));
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
class PathFinder {
|
||||
public:
|
||||
struct NavigationArea {
|
||||
std::string name;
|
||||
bool available = true;
|
||||
std::vector<Vec2fp> polygon;
|
||||
};
|
||||
|
||||
struct ObstaclePolygon {
|
||||
std::string name;
|
||||
std::vector<Vec2fp> polygon;
|
||||
};
|
||||
|
||||
struct NavTriangle {
|
||||
int id = -1;
|
||||
Vec2fp vertices[3];
|
||||
Vec2fp center;
|
||||
int neighbors[3] = { -1, -1, -1 }; // Индексы соседних треугольников по ребрам (0-1, 1-2, 2-0)
|
||||
};
|
||||
|
||||
struct Portal {
|
||||
Vec2fp left;
|
||||
Vec2fp right;
|
||||
};
|
||||
|
||||
// Масштаб для Clipper2 (так как Clipper работает с целыми int64_t,
|
||||
// сохраняем точность в 10000 единиц на 1 метр)
|
||||
static constexpr int64_t CLIPPER_SCALE = 10000;
|
||||
|
||||
void build(const std::string& configPath = "", const std::string& zipPath = "") {
|
||||
// Здесь может быть парсинг configPath в area и obstaclePolygons.
|
||||
// После заполнения геометрии строим NavMesh:
|
||||
area.name = "main";
|
||||
area.polygon = {
|
||||
Vec2fp::fromFloat(-100.f, -100.f),
|
||||
Vec2fp::fromFloat(-100.f, 100.f),
|
||||
Vec2fp::fromFloat(100.f, 100.f),
|
||||
Vec2fp::fromFloat(100.f, -100.f)
|
||||
};
|
||||
|
||||
area.available = true;
|
||||
|
||||
obstaclePolygons.push_back({
|
||||
"p1", {Vec2fp::fromFloat(-10.f, -10.f),
|
||||
Vec2fp::fromFloat(-10.f, 10.f),
|
||||
Vec2fp::fromFloat(10.f, 10.f),
|
||||
Vec2fp::fromFloat(10.f, -10.f)}
|
||||
});
|
||||
|
||||
//ready = true;
|
||||
|
||||
generateNavMesh();
|
||||
}
|
||||
|
||||
// Построение NavMesh: Объединение полигонов (Clipper2) + Триангуляция (CDT)
|
||||
void generateNavMesh() {
|
||||
navMesh.clear();
|
||||
if (area.polygon.size() < 3) return;
|
||||
|
||||
using namespace Clipper2Lib;
|
||||
|
||||
// Вспомогательная лямбда: безопасно переводит fp в координаты Clipper2 (int64_t)
|
||||
auto toClipperCoord = [](fp val) -> int64_t {
|
||||
return (static_cast<int64_t>(val.raw_value()) * CLIPPER_SCALE) / (1LL << FP_FRACTION_BITS);
|
||||
};
|
||||
|
||||
// 1. Переводим контур локации (NavigationArea) в формат Clipper2
|
||||
Paths64 subject;
|
||||
Path64 outerPath;
|
||||
for (const auto& pt : area.polygon) {
|
||||
outerPath.push_back(Point64(toClipperCoord(pt.x), toClipperCoord(pt.y)));
|
||||
}
|
||||
subject.push_back(outerPath);
|
||||
|
||||
// 2. Собираем все препятствия
|
||||
Paths64 clips;
|
||||
for (const auto& obs : obstaclePolygons) {
|
||||
if (obs.polygon.size() < 3) continue;
|
||||
Path64 clipPath;
|
||||
for (const auto& pt : obs.polygon) {
|
||||
clipPath.push_back(Point64(toClipperCoord(pt.x), toClipperCoord(pt.y)));
|
||||
}
|
||||
clips.push_back(clipPath);
|
||||
}
|
||||
|
||||
// 3. Булево объединение пересекающихся препятствий и вычитание их из локации
|
||||
// Clipper автоматически объединяет пересекающиеся полигоны внутри Difference
|
||||
PolyTree64 solTree;
|
||||
Clipper64 clipper;
|
||||
clipper.AddSubject(subject);
|
||||
clipper.AddClip(clips);
|
||||
//clipper.Execute(ClipType::Difference, FillRule::NonZero, solTree);
|
||||
clipper.Execute(ClipType::Difference, FillRule::EvenOdd, solTree);
|
||||
|
||||
// 4. Подготовка данных для Constrained Delaunay Triangulation (CDT)
|
||||
std::vector<CDT::V2d<double>> cdtVertices;
|
||||
std::vector<CDT::Edge> cdtEdges;
|
||||
|
||||
auto extractPolygons = [&](const PolyPath64* node, auto& selfRef) -> void {
|
||||
if (!node->Polygon().empty()) {
|
||||
const Path64& path = node->Polygon();
|
||||
std::size_t startIdx = cdtVertices.size();
|
||||
std::size_t count = path.size();
|
||||
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
double x = static_cast<double>(path[i].x) / CLIPPER_SCALE;
|
||||
double y = static_cast<double>(path[i].y) / CLIPPER_SCALE;
|
||||
cdtVertices.push_back({ x, y });
|
||||
cdtEdges.push_back(CDT::Edge(startIdx + i, startIdx + (i + 1) % count));
|
||||
}
|
||||
}
|
||||
for (std::size_t i = 0; i < node->Count(); ++i) {
|
||||
selfRef(node->Child(i), selfRef);
|
||||
}
|
||||
};
|
||||
|
||||
for (std::size_t i = 0; i < solTree.Count(); ++i) {
|
||||
extractPolygons(solTree.Child(i), extractPolygons);
|
||||
}
|
||||
|
||||
if (cdtVertices.empty()) return;
|
||||
|
||||
CDT::RemoveDuplicatesAndRemapEdges(cdtVertices, cdtEdges);
|
||||
|
||||
// 5. Выполняем триангуляцию и удаляем треугольники внутри дыр (препятствий)
|
||||
CDT::Triangulation<double> cdt;
|
||||
cdt.insertVertices(cdtVertices);
|
||||
cdt.insertEdges(cdtEdges);
|
||||
cdt.eraseOuterTrianglesAndHoles();
|
||||
|
||||
// 6. Конвертируем результат в детерминированный граф NavTriangle (Q32.32)
|
||||
const auto& triangles = cdt.triangles;
|
||||
const auto& vertices = cdt.vertices;
|
||||
navMesh.resize(triangles.size());
|
||||
|
||||
for (std::size_t i = 0; i < triangles.size(); ++i) {
|
||||
NavTriangle& navTri = navMesh[i];
|
||||
navTri.id = static_cast<int>(i);
|
||||
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
const auto& v = vertices[triangles[i].vertices[j]];
|
||||
navTri.vertices[j] = Vec2fp(fp(v.x), fp(v.y));
|
||||
|
||||
// Соседи в CDT хранятся напротив вершин, перепривязываем к ребрам (j, j+1)
|
||||
auto neighborIdx = triangles[i].neighbors[j];
|
||||
navTri.neighbors[j] = (neighborIdx != CDT::noNeighbor) ? static_cast<int>(neighborIdx) : -1;
|
||||
}
|
||||
|
||||
navTri.center = (navTri.vertices[0] + navTri.vertices[1] + navTri.vertices[2]) / fp(3);
|
||||
}
|
||||
|
||||
ready = !navMesh.empty();
|
||||
}
|
||||
|
||||
// Основной метод поиска пути (плоскость XZ, Y — высота)
|
||||
std::vector<Vec3fp> findPathToNearest(const Vec3fp& start3D, const Vec3fp& end3D) const {
|
||||
if (!ready || !area.available) return { start3D };
|
||||
|
||||
Vec2fp startPt(start3D.x, start3D.z);
|
||||
Vec2fp targetPt(end3D.x, end3D.z);
|
||||
|
||||
// 1. Находим треугольники для старта и финиша.
|
||||
// Если точка попала в препятствие (вне NavMesh), ищем ближайшую точку на ребрах NavMesh
|
||||
int startTri = findTriangleContaining(startPt);
|
||||
if (startTri == -1) {
|
||||
startPt = findClosestPointOnNavMesh(startPt, startTri);
|
||||
}
|
||||
|
||||
int endTri = findTriangleContaining(targetPt);
|
||||
if (endTri == -1) {
|
||||
targetPt = findClosestPointOnNavMesh(targetPt, endTri);
|
||||
}
|
||||
|
||||
if (startTri == -1 || endTri == -1) return { start3D };
|
||||
|
||||
// Если старт и конец в одном треугольнике — идем по прямой
|
||||
if (startTri == endTri) {
|
||||
return {
|
||||
start3D,
|
||||
Vec3fp(targetPt.x, floorY, targetPt.y)
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Поиск коридора из треугольников через A*
|
||||
std::vector<int> triangleCorridor = runAStar(startTri, endTri, startPt, targetPt);
|
||||
if (triangleCorridor.empty()) return { start3D };
|
||||
|
||||
// 3. Сглаживание пути через Funnel Algorithm (Натянутая нить)
|
||||
std::vector<Vec2fp> smoothPath2D = stringPullFunnel(startPt, targetPt, triangleCorridor);
|
||||
|
||||
// 4. Формируем выходной массив 3D точек
|
||||
std::vector<Vec3fp> result;
|
||||
result.reserve(smoothPath2D.size());
|
||||
for (const auto& pt : smoothPath2D) {
|
||||
result.emplace_back(pt.x, floorY, pt.y);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Перегрузка для совместимости со старым Eigen-интерфейсом
|
||||
std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start, const Eigen::Vector3f& end) const {
|
||||
auto pathFp = findPathToNearest(Vec3fp::fromEigen(start), Vec3fp::fromEigen(end));
|
||||
std::vector<Eigen::Vector3f> out;
|
||||
out.reserve(pathFp.size());
|
||||
for (const auto& p : pathFp) out.push_back(p.toEigen());
|
||||
return out;
|
||||
}
|
||||
|
||||
bool isWalkable(const Vec3fp& point) const {
|
||||
return findTriangleContaining(Vec2fp(point.x, point.z)) != -1;
|
||||
}
|
||||
|
||||
bool isWalkable(const Eigen::Vector3f& point) const {
|
||||
return isWalkable(Vec3fp::fromEigen(point));
|
||||
}
|
||||
|
||||
bool setAreaAvailable(const std::string& areaName, bool available) {
|
||||
if (area.name == areaName) {
|
||||
area.available = available;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isReady() const { return ready; }
|
||||
const NavigationArea& getArea() const { return area; }
|
||||
void setArea(const NavigationArea& a) { area = a; }
|
||||
|
||||
fp getFloorY() const { return floorY; }
|
||||
void setFloorY(fp y) { floorY = y; }
|
||||
|
||||
void addObstaclePolygon(const ObstaclePolygon& polygon) {
|
||||
obstaclePolygons.push_back(polygon);
|
||||
}
|
||||
const std::vector<ObstaclePolygon>& getObstaclePolygons() const { return obstaclePolygons; }
|
||||
|
||||
bool saveConfig(const std::string& path) const { return true; }
|
||||
bool saveGrid(const std::string& path) const { return true; }
|
||||
|
||||
// Внутри public: класса PathFinder
|
||||
Vec2fp clampToNavMesh(const Vec2fp& pt, int& outTriId) const {
|
||||
return findClosestPointOnNavMesh(pt, outTriId);
|
||||
}
|
||||
|
||||
// Удобная перегрузка без outTriId, если индекс треугольника не нужен:
|
||||
Vec2fp clampToNavMesh(const Vec2fp& pt) const {
|
||||
int dummyTri = -1;
|
||||
return findClosestPointOnNavMesh(pt, dummyTri);
|
||||
}
|
||||
|
||||
protected:
|
||||
NavigationArea area;
|
||||
std::vector<ObstaclePolygon> obstaclePolygons;
|
||||
std::vector<NavTriangle> navMesh;
|
||||
fp floorY{ 0 };
|
||||
bool ready = false;
|
||||
|
||||
// Проверка нахождения точки внутри треугольника (через знаки векторных произведений)
|
||||
static bool isPointInTriangle(const Vec2fp& p, const Vec2fp& a, const Vec2fp& b, const Vec2fp& c) {
|
||||
fp d1 = (b - a).cross(p - a);
|
||||
fp d2 = (c - b).cross(p - b);
|
||||
fp d3 = (a - c).cross(p - c);
|
||||
|
||||
bool hasNeg = (d1 < fp(0)) || (d2 < fp(0)) || (d3 < fp(0));
|
||||
bool hasPos = (d1 > fp(0)) || (d2 > fp(0)) || (d3 > fp(0));
|
||||
return !(hasNeg && hasPos);
|
||||
}
|
||||
|
||||
int findTriangleContaining(const Vec2fp& pt) const {
|
||||
for (const auto& tri : navMesh) {
|
||||
if (isPointInTriangle(pt, tri.vertices[0], tri.vertices[1], tri.vertices[2])) {
|
||||
return tri.id;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Проекция точки на отрезок AB
|
||||
static Vec2fp closestPointOnSegment(const Vec2fp& p, const Vec2fp& a, const Vec2fp& b) {
|
||||
Vec2fp ab = b - a;
|
||||
fp lenSq = ab.lengthSq();
|
||||
if (lenSq == fp(0)) return a;
|
||||
|
||||
fp t = (p - a).dot(ab) / lenSq;
|
||||
t = std::max(fp(0), std::min(fp(1), t));
|
||||
return a + ab * t;
|
||||
}
|
||||
|
||||
Vec2fp findClosestPointOnNavMesh(const Vec2fp& pt, int& outTriId) const {
|
||||
fp minDistSq = std::numeric_limits<fp>::max();
|
||||
Vec2fp bestPt = pt;
|
||||
outTriId = -1;
|
||||
|
||||
for (const auto& tri : navMesh) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
// В CDT neighbors[i] — это сосед через ребро (vertices[i], vertices[(i + 1) % 3])
|
||||
if (tri.neighbors[i] == -1) {
|
||||
const Vec2fp& a = tri.vertices[i];
|
||||
const Vec2fp& b = tri.vertices[(i + 1) % 3];
|
||||
Vec2fp proj = closestPointOnSegment(pt, a, b);
|
||||
fp distSq = (pt - proj).lengthSq();
|
||||
|
||||
if (distSq < minDistSq) {
|
||||
minDistSq = distSq;
|
||||
bestPt = proj;
|
||||
outTriId = tri.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bestPt;
|
||||
}
|
||||
|
||||
// Поиск пути A* по центрам треугольников
|
||||
std::vector<int> runAStar(int startTri, int endTri, const Vec2fp& startPt, const Vec2fp& endPt) const {
|
||||
struct NodeRecord {
|
||||
int triId;
|
||||
fp fScore;
|
||||
// В случае равенства fScore сортируем по triId для 100% детерминизма
|
||||
bool operator>(const NodeRecord& o) const {
|
||||
if (fScore != o.fScore) return fScore > o.fScore;
|
||||
return triId > o.triId;
|
||||
}
|
||||
};
|
||||
|
||||
std::priority_queue<NodeRecord, std::vector<NodeRecord>, std::greater<NodeRecord>> openSet;
|
||||
std::vector<fp> gScore(navMesh.size(), std::numeric_limits<fp>::max());
|
||||
std::vector<int> cameFrom(navMesh.size(), -1);
|
||||
std::vector<bool> closed(navMesh.size(), false);
|
||||
|
||||
gScore[startTri] = fp(0);
|
||||
openSet.push({ startTri, (navMesh[startTri].center - endPt).length() });
|
||||
|
||||
while (!openSet.empty()) {
|
||||
int current = openSet.top().triId;
|
||||
openSet.pop();
|
||||
|
||||
if (current == endTri) {
|
||||
std::vector<int> path;
|
||||
for (int curr = endTri; curr != -1; curr = cameFrom[curr]) {
|
||||
path.push_back(curr);
|
||||
}
|
||||
std::reverse(path.begin(), path.end());
|
||||
return path;
|
||||
}
|
||||
|
||||
if (closed[current]) continue;
|
||||
closed[current] = true;
|
||||
|
||||
const NavTriangle& currTri = navMesh[current];
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
int neighbor = currTri.neighbors[i];
|
||||
if (neighbor == -1 || closed[neighbor]) continue;
|
||||
|
||||
fp stepCost = (currTri.center - navMesh[neighbor].center).length();
|
||||
fp tentativeG = gScore[current] + stepCost;
|
||||
|
||||
if (tentativeG < gScore[neighbor]) {
|
||||
cameFrom[neighbor] = current;
|
||||
gScore[neighbor] = tentativeG;
|
||||
fp h = (navMesh[neighbor].center - endPt).length();
|
||||
openSet.push({ neighbor, tentativeG + h });
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Извлечение общих вершин (портала) между двумя соседними треугольниками
|
||||
// Извлечение общих вершин (портала) между двумя соседними треугольниками
|
||||
Portal getPortal(int fromTriId, int toTriId) const {
|
||||
const NavTriangle& t1 = navMesh[fromTriId];
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (t1.neighbors[i] == toTriId) {
|
||||
// При обходе треугольника CCW и движении изнутри наружу через ребро i -> (i + 1) % 3:
|
||||
// вершина i находится СЛЕВА, а вершина (i + 1) % 3 — СПРАВА
|
||||
return Portal{ t1.vertices[i], t1.vertices[(i + 1) % 3] };
|
||||
}
|
||||
}
|
||||
return Portal{ t1.center, t1.center };
|
||||
}
|
||||
|
||||
// Алгоритм натянутой нити (Simple Stupid Funnel Algorithm)
|
||||
std::vector<Vec2fp> stringPullFunnel(const Vec2fp& startPt, const Vec2fp& endPt, const std::vector<int>& corridor) const {
|
||||
std::vector<Portal> portals;
|
||||
portals.reserve(corridor.size() + 2);
|
||||
portals.push_back({ startPt, startPt });
|
||||
|
||||
for (std::size_t i = 0; i + 1 < corridor.size(); ++i) {
|
||||
portals.push_back(getPortal(corridor[i], corridor[i + 1]));
|
||||
}
|
||||
portals.push_back({ endPt, endPt });
|
||||
|
||||
std::vector<Vec2fp> pts;
|
||||
pts.push_back(startPt);
|
||||
|
||||
Vec2fp portalApex = startPt;
|
||||
Vec2fp portalLeft = startPt;
|
||||
Vec2fp portalRight = startPt;
|
||||
int apexIndex = 0, leftIndex = 0, rightIndex = 0;
|
||||
|
||||
for (int i = 1; i < static_cast<int>(portals.size()); ++i) {
|
||||
const Vec2fp& left = portals[i].left;
|
||||
const Vec2fp& right = portals[i].right;
|
||||
|
||||
// Обновляем правую границу воронки
|
||||
if ((portalRight - portalApex).cross(right - portalApex) <= fp(0)) {
|
||||
if (portalApex == portalRight || (portalLeft - portalApex).cross(right - portalApex) > fp(0)) {
|
||||
portalRight = right;
|
||||
rightIndex = i;
|
||||
}
|
||||
else {
|
||||
pts.push_back(portalLeft);
|
||||
portalApex = portalLeft;
|
||||
apexIndex = leftIndex;
|
||||
portalLeft = portalApex;
|
||||
portalRight = portalApex;
|
||||
leftIndex = apexIndex;
|
||||
rightIndex = apexIndex;
|
||||
i = apexIndex;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем левую границу воронки
|
||||
if ((portalLeft - portalApex).cross(left - portalApex) >= fp(0)) {
|
||||
if (portalApex == portalLeft || (portalRight - portalApex).cross(left - portalApex) < fp(0)) {
|
||||
portalLeft = left;
|
||||
leftIndex = i;
|
||||
}
|
||||
else {
|
||||
pts.push_back(portalRight);
|
||||
portalApex = portalRight;
|
||||
apexIndex = rightIndex;
|
||||
portalLeft = portalApex;
|
||||
portalRight = portalApex;
|
||||
leftIndex = apexIndex;
|
||||
rightIndex = apexIndex;
|
||||
i = apexIndex;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pts.back() != endPt) {
|
||||
pts.push_back(endPt);
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
class PathFinderOld {
|
||||
public:
|
||||
|
||||
struct ObstaclePolygon {
|
||||
@@ -39,18 +611,18 @@ public:
|
||||
std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end) const;
|
||||
|
||||
std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
|
||||
/*std::vector<Eigen::Vector3f> findPath(const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end,
|
||||
const std::vector<DynamicObstacle>& dynamicObstacles) const;
|
||||
|
||||
*/
|
||||
// Like findPath, but when the destination is unreachable the character
|
||||
// walks as close as possible instead of not moving at all.
|
||||
std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end) const;
|
||||
std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start,
|
||||
/*std::vector<Eigen::Vector3f> findPathToNearest(const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end,
|
||||
const std::vector<DynamicObstacle>& dynamicObstacles) const;
|
||||
|
||||
*/
|
||||
bool setAreaAvailable(const std::string& areaName, bool available);
|
||||
bool isReady() const { return ready; }
|
||||
bool isWalkable(const Eigen::Vector3f& point) const;
|
||||
|
||||
@@ -109,7 +109,7 @@ namespace FRG {
|
||||
const size_t firstWaypoint = (std::min)(
|
||||
state->currentWaypointIndex, state->pathWaypoints.size());
|
||||
for (size_t i = firstWaypoint; i < state->pathWaypoints.size(); ++i) {
|
||||
const Eigen::Vector3f& waypoint = state->pathWaypoints[i];
|
||||
const Eigen::Vector3f& waypoint = state->pathWaypoints[i].toEigen();
|
||||
route.push_back(Eigen::Vector3f(waypoint.x(), 0.0f, waypoint.z()));
|
||||
}
|
||||
const Eigen::Vector3f clickedPosition(
|
||||
@@ -140,7 +140,7 @@ namespace FRG {
|
||||
constexpr float kPlayerClearanceMs = 20.0f;
|
||||
constexpr float kPlayerMaximumSpeedMultiplier = 1.5f;
|
||||
const float playerSpeed = (std::max)(
|
||||
state->walkSpeed * kPlayerMaximumSpeedMultiplier, 0.01f);
|
||||
static_cast<float>(state->walkSpeed) * kPlayerMaximumSpeedMultiplier, 0.01f);
|
||||
const float playerRouteTimeMs = totalLength / playerSpeed * 1000.0f;
|
||||
const float revealDurationMs = (std::min)(kMaximumRevealMs, playerRouteTimeMs / 3.0f);
|
||||
const float waveSpeed = totalLength / (revealDurationMs / 1000.0f);
|
||||
|
||||
Reference in new issue
Block a user