diff --git a/proj-web/CMakeLists.txt b/proj-web/CMakeLists.txt
index e41f118..8b02c39 100644
--- a/proj-web/CMakeLists.txt
+++ b/proj-web/CMakeLists.txt
@@ -115,12 +115,15 @@ set(EMSCRIPTEN_FLAGS
target_compile_options(space-game001 PRIVATE ${EMSCRIPTEN_FLAGS} "-O2")
+# Only loading.png and the shaders used before resources.zip is ready are preloaded.
+# resources.zip is downloaded asynchronously at runtime and served as a separate file.
set(EMSCRIPTEN_LINK_FLAGS
${EMSCRIPTEN_FLAGS}
"-O2"
"-sPTHREAD_POOL_SIZE=4"
"-sALLOW_MEMORY_GROWTH=1"
- "--preload-file resources.zip"
+ "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/loading.png@resources/loading.png"
+ "--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/../resources/shaders@resources/shaders"
)
# Применяем настройки линковки
@@ -170,8 +173,8 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/space-game001plain.html"
DESTINATION .
)
-# Если вам все еще нужен сам resources.zip отдельно в папке public:
-#install(FILES "${RESOURCES_ZIP}" DESTINATION .)
+# resources.zip is served separately and downloaded asynchronously at runtime
+install(FILES "${RESOURCES_ZIP}" DESTINATION .)
add_custom_command(TARGET space-game001 POST_BUILD
COMMAND ${CMAKE_COMMAND} --install .
diff --git a/proj-web/space-game001plain.html b/proj-web/space-game001plain.html
index 17708db..72892cd 100644
--- a/proj-web/space-game001plain.html
+++ b/proj-web/space-game001plain.html
@@ -1,100 +1,76 @@
-
-
+
+
+
+
+
+ Space Game
+
+
+
+
+ Downloading...
+
-
-
-
- Space Game
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
\ No newline at end of file
diff --git a/resources/Cargo_Base_color_sRGB.png b/resources/Cargo_Base_color_sRGB.png
index 6ec9e8e..493514c 100644
--- a/resources/Cargo_Base_color_sRGB.png
+++ b/resources/Cargo_Base_color_sRGB.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6bd5d071ed94f2bd8ce3ab060136e9b93b04b06d5eabaffc7845a99d73faeb30
-size 2345111
+oid sha256:d8505521fa1598d9140e518deabcc7c20b226b90a7758e1b1ff5795c9a3b73a5
+size 2890059
diff --git a/resources/DefaultMaterial_BaseColor_shine.png b/resources/DefaultMaterial_BaseColor_shine.png
deleted file mode 100644
index 53e6f41..0000000
--- a/resources/DefaultMaterial_BaseColor_shine.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:26c118a182e269aebfa22684489836d8632b0b2cb6631be646c2678c18493463
-size 90539
diff --git a/resources/MainCharacter_Base_color_sRGB.png b/resources/MainCharacter_Base_color_sRGB.png
index 73d132c..9c074e3 100644
--- a/resources/MainCharacter_Base_color_sRGB.png
+++ b/resources/MainCharacter_Base_color_sRGB.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:dc8c7262949a4f2f86d2cc47daf572d908294868d89f7577a771149c7e2e60e5
-size 1296067
+oid sha256:69a783d983e5356aa0559f0f333ed6a083d4e5c9cd6190bf68a28d122af66ec8
+size 2823280
diff --git a/resources/config/crosshair_config.json b/resources/config/crosshair_config.json
new file mode 100644
index 0000000..d4b2452
--- /dev/null
+++ b/resources/config/crosshair_config.json
@@ -0,0 +1,21 @@
+{
+ "enabled": true,
+
+ "referenceResolution": [1280, 720],
+
+ "color": [1.0, 1.0, 1.0],
+ "cl_crosshairalpha": 1.0,
+ "cl_crosshairthickness": 2.0,
+
+ "centerGapPx": 10.0,
+
+ "top": {
+ "lengthPx": 14.0,
+ "angleDeg": 90.0
+ },
+
+ "arms": [
+ { "lengthPx": 20.0, "angleDeg": 210.0 },
+ { "lengthPx": 20.0, "angleDeg": 330.0 }
+ ]
+}
\ No newline at end of file
diff --git a/resources/config/game_over.json b/resources/config/game_over.json
index fe448c6..6248579 100644
--- a/resources/config/game_over.json
+++ b/resources/config/game_over.json
@@ -1,55 +1,85 @@
{
- "root": {
- "type": "LinearLayout",
- "orientation": "vertical",
- "align": "center",
- "x": 0,
- "y": 0,
- "width": 1920,
- "height": 1080,
- "background": {
- "color": [0, 0, 0, 0.7]
- },
- "children": [
- {
- "type": "Button",
- "name": "gameOverText",
- "x": 350,
- "y": 400,
- "width": 600,
- "height": 150,
- "textures": {
- "normal": "resources/gameover.png",
- "hover": "resources/gameover.png",
- "pressed": "resources/gameover.png"
- }
- },
- {
- "type": "Button",
- "name": "restartButton",
- "x": 350,
- "y": 300,
- "width": 300,
- "height": 80,
- "textures": {
- "normal": "resources/shoot_normal.png",
- "hover": "resources/shoot_normal.png",
- "pressed": "resources/shoot_normal.png"
- }
- },
- {
- "type": "Button",
- "name": "gameOverExitButton",
- "x": 650,
- "y": 300,
- "width": 300,
- "height": 80,
- "textures": {
- "normal": "resources/sand2.png",
- "hover": "resources/sand2.png",
- "pressed": "resources/sand2.png"
- }
- }
- ]
- }
+ "root": {
+ "type": "LinearLayout",
+ "orientation": "vertical",
+ "vertical_align": "center",
+ "horizontal_align": "center",
+ "spacing": 10,
+ "x": 0,
+ "y": 0,
+ "width": "match_parent",
+ "height": "match_parent",
+ "children": [
+ {
+ "type": "Button",
+ "name": "gameOverText",
+ "width": 327,
+ "height": 26,
+ "textures": {
+ "normal": "resources/game_over/MissionFailed.png",
+ "hover": "resources/game_over/MissionFailed.png",
+ "pressed": "resources/game_over/MissionFailed.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "underlineBtn",
+ "width": 168,
+ "height": 44,
+ "textures": {
+ "normal": "resources/game_over/Container.png",
+ "hover": "resources/game_over/Container.png",
+ "pressed": "resources/game_over/Container.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "finalscore",
+ "width": 87,
+ "height": 9,
+ "textures": {
+ "normal": "resources/game_over/FinalScore.png",
+ "hover": "resources/game_over/FinalScore.png",
+ "pressed": "resources/game_over/FinalScore.png"
+ }
+ },
+ {
+ "type": "TextView",
+ "name": "scoreText",
+ "width": 600,
+ "height": 80,
+ "text": "0",
+ "fontSize": 36,
+ "color": [
+ 0,
+ 217,
+ 255,
+ 1
+ ],
+ "align": "center"
+ },
+ {
+ "type": "Button",
+ "name": "restartButton",
+ "width": 382,
+ "height": 56,
+ "textures": {
+ "normal": "resources/game_over/Filledbuttons.png",
+ "hover": "resources/game_over/Filledbuttons.png",
+ "pressed": "resources/game_over/Filledbuttons.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "gameOverExitButton",
+ "width": 382,
+ "height": 56,
+ "textures": {
+ "normal": "resources/game_over/Secondarybutton.png",
+ "hover": "resources/game_over/Secondarybutton.png",
+ "pressed": "resources/game_over/Secondarybutton.png"
+ }
+ }
+ ]
+ }
}
\ No newline at end of file
diff --git a/resources/config/game_over_old.json b/resources/config/game_over_old.json
new file mode 100644
index 0000000..4fcac9d
--- /dev/null
+++ b/resources/config/game_over_old.json
@@ -0,0 +1,93 @@
+{
+ "root": {
+ "type": "LinearLayout",
+ "orientation": "vertical",
+ "align": "center",
+ "x": 0,
+ "y": 0,
+ "width": 1920,
+ "height": 1080,
+ "background": {
+ "color": [0, 0, 0, 0.7]
+ },
+ "children": [
+ {
+ "type": "Button",
+ "name": "gameOverText",
+ "x": 476.5,
+ "y": 500,
+ "width": 327,
+ "height": 26,
+ "textures": {
+ "normal": "resources/game_over/MissionFailed.png",
+ "hover": "resources/game_over/MissionFailed.png",
+ "pressed": "resources/game_over/MissionFailed.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "underlineBtn",
+ "x": 556,
+ "y": 465,
+ "width": 168,
+ "height": 44,
+ "textures": {
+ "normal": "resources/game_over/Container.png",
+ "hover": "resources/game_over/Container.png",
+ "pressed": "resources/game_over/Container.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "finalscore",
+ "x": 596.5,
+ "y": 436,
+ "width": 87,
+ "height": 9,
+ "textures": {
+ "normal": "resources/game_over/FinalScore.png",
+ "hover": "resources/game_over/FinalScore.png",
+ "pressed": "resources/game_over/FinalScore.png"
+ }
+ },
+ {
+ "type": "TextView",
+ "name": "scoreText",
+ "x": 350,
+ "y": 356,
+ "width": 600,
+ "height": 80,
+ "text": "0",
+ "fontSize": 36,
+ "color": [0, 217, 255, 1],
+ "align": "center"
+ },
+ {
+ "type": "Button",
+ "name": "restartButton",
+ "x": 449,
+ "y": 308,
+ "width": 382,
+ "height": 56,
+ "textures": {
+ "normal": "resources/game_over/Filledbuttons.png",
+ "hover": "resources/game_over/Filledbuttons.png",
+ "pressed": "resources/game_over/Filledbuttons.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "gameOverExitButton",
+ "x": 449,
+ "y": 240,
+ "width": 382,
+ "height": 56,
+ "textures": {
+ "normal": "resources/game_over/Secondarybutton.png",
+ "hover": "resources/game_over/Secondarybutton.png",
+ "pressed": "resources/game_over/Secondarybutton.png"
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/resources/config/main_menu.json b/resources/config/main_menu.json
index 1807e74..55bbb1f 100644
--- a/resources/config/main_menu.json
+++ b/resources/config/main_menu.json
@@ -1,141 +1,81 @@
{
"root": {
- "type": "FrameLayout",
- "x": 0,
- "y": 0,
- "width": 1280,
- "height": 720,
- "children": [
- {
- "type": "LinearLayout",
- "name": "settingsButtons",
- "orientation": "vertical",
- "spacing": 10,
- "x": 0,
- "y": 0,
- "width": 300,
- "height": 300,
- "children": [
- {
- "type": "Button",
- "name": "langButton",
- "x": 1100,
- "y": 580,
- "width": 142,
- "height": 96,
- "textures": {
- "normal": "resources/main_menu/lang.png",
- "hover": "resources/main_menu/lang.png",
- "pressed": "resources/main_menu/lang.png"
- }
- },
- {
- "type": "Button",
- "name": "titleBtn",
- "x": 473,
- "y": 500,
- "width": 254,
- "height": 35,
- "textures": {
+ "type": "LinearLayout",
+ "orientation": "vertical",
+ "vertical_align": "center",
+ "horizontal_align": "center",
+ "spacing": 10,
+ "x": 0,
+ "y": 0,
+ "width": "match_parent",
+ "height": "match_parent",
+ "children": [
+ {
+ "type": "Button",
+ "name": "titleBtn",
+ "width": 254,
+ "height": 35,
+ "textures": {
"normal": "resources/main_menu/title.png",
"hover": "resources/main_menu/title.png",
"pressed": "resources/main_menu/title.png"
- }
- },
- {
- "type": "Button",
- "name": "underlineBtn",
- "x": 516,
- "y": 465,
- "width": 168,
- "height": 44,
- "textures": {
+ }
+ },
+ {
+ "type": "Button",
+ "name": "underlineBtn",
+ "width": 168,
+ "height": 44,
+ "textures": {
"normal": "resources/main_menu/line.png",
"hover": "resources/main_menu/line.png",
"pressed": "resources/main_menu/line.png"
- }
- },
- {
- "type": "Button",
- "name": "subtitleBtn",
- "x": 528,
- "y": 455,
- "width": 144,
- "height": 11,
- "textures": {
+ }
+ },
+ {
+ "type": "Button",
+ "name": "subtitleBtn",
+ "width": 144,
+ "height": 11,
+ "textures": {
"normal": "resources/main_menu/subtitle.png",
"hover": "resources/main_menu/subtitle.png",
"pressed": "resources/main_menu/subtitle.png"
- }
- },
- {
- "type": "Button",
- "name": "singleButton",
- "x": 409,
- "y": 360,
- "width": 382,
- "height": 56,
- "textures": {
+ }
+ },
+ {
+ "type": "Button",
+ "name": "singleButton",
+ "width": 382,
+ "height": 56,
+ "textures": {
"normal": "resources/main_menu/single.png",
"hover": "resources/main_menu/single.png",
"pressed": "resources/main_menu/single.png"
- }
- },
- {
- "type": "Button",
- "name": "multiplayerButton",
- "x": 409,
- "y": 289,
- "width": 382,
- "height": 56,
- "textures": {
+ }
+ },
+ {
+ "type": "Button",
+ "name": "multiplayerButton",
+ "width": 382,
+ "height": 56,
+ "textures": {
"normal": "resources/main_menu/multi.png",
"hover": "resources/main_menu/multi.png",
"pressed": "resources/main_menu/multi.png"
- }
- },
- {
- "type": "Button",
- "name": "multiplayerButton2",
- "x": 409,
- "y": 218,
- "width": 382,
- "height": 56,
- "textures": {
- "normal": "resources/main_menu/multi.png",
- "hover": "resources/main_menu/multi.png",
- "pressed": "resources/main_menu/multi.png"
- }
- },
- {
- "type": "Button",
- "name": "exitButton",
- "x": 409,
- "y": 147,
- "width": 382,
- "height": 56,
- "textures": {
- "normal": "resources/main_menu/exit.png",
- "hover": "resources/main_menu/exit.png",
- "pressed": "resources/main_menu/exit.png"
- }
- },
- {
- "type": "Button",
- "name": "versionLabel",
- "x": 559.5,
- "y": 99,
- "width": 81,
- "height": 9,
- "textures": {
+ }
+ },
+ {
+ "type": "Button",
+ "name": "versionLabel",
+ "width": 81,
+ "height": 9,
+ "textures": {
"normal": "resources/main_menu/version.png",
"hover": "resources/main_menu/version.png",
"pressed": "resources/main_menu/version.png"
- }
}
- ]
}
- ]
- }
+ ]
}
-
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/resources/config/multiplayer_menu.json b/resources/config/multiplayer_menu.json
index 17bf917..acbbe09 100644
--- a/resources/config/multiplayer_menu.json
+++ b/resources/config/multiplayer_menu.json
@@ -3,103 +3,156 @@
"type": "LinearLayout",
"x": 0,
"y": 0,
- "width": 1920,
- "height": 1080,
- "orientation": "vertical",
- "spacing": 20,
+ "width": 1280,
+ "height": 720,
"children": [
{
- "type": "TextView",
- "name": "titleText",
- "x": 300,
- "y": 100,
- "width": 1320,
- "height": 100,
- "text": "Multiplayer",
- "fontPath": "resources/fonts/DroidSans.ttf",
- "fontSize": 72,
- "color": [1, 1, 1, 1],
- "centered": true
- },
- {
- "type": "TextView",
- "name": "serverLabel",
- "x": 400,
- "y": 250,
- "width": 1120,
- "height": 50,
- "text": "Enter server name or IP:",
- "fontPath": "resources/fonts/DroidSans.ttf",
- "fontSize": 32,
- "color": [1, 1, 1, 1],
- "centered": false
- },
+ "type": "Button",
+ "name": "langButton",
+ "x": 1100,
+ "y": 580,
+ "width": 142,
+ "height": 96,
+ "textures": {
+ "normal": "resources/main_menu/lang.png",
+ "hover": "resources/main_menu/lang.png",
+ "pressed": "resources/main_menu/lang.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "titleBtn",
+ "x": 512,
+ "y": 500,
+ "width": 254,
+ "height": 35,
+ "textures": {
+ "normal": "resources/multiplayer_menu/title.png",
+ "hover": "resources/multiplayer_menu/title.png",
+ "pressed": "resources/multiplayer_menu/title.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "subtitle",
+ "x": 596.5,
+ "y": 470,
+ "width": 87,
+ "height": 11,
+ "textures": {
+ "normal": "resources/multiplayer_menu/JoinServer.png",
+ "hover": "resources/multiplayer_menu/JoinServer.png",
+ "pressed": "resources/multiplayer_menu/JoinServer.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "subtitleBtn",
+ "x": 450,
+ "y": 445,
+ "width": 94,
+ "height": 9,
+ "textures": {
+ "normal": "resources/multiplayer_menu/ServerName.png",
+ "hover": "resources/multiplayer_menu/ServerName.png",
+ "pressed": "resources/multiplayer_menu/ServerName.png"
+ }
+ },
{
"type": "TextField",
"name": "serverInputField",
- "x": 400,
- "y": 320,
- "width": 1120,
- "height": 60,
+ "x": 449,
+ "y": 390,
+ "width": 382,
+ "height": 56,
"placeholder": "Enter server name or IP",
"fontPath": "resources/fonts/DroidSans.ttf",
- "fontSize": 28,
+ "fontSize": 16,
"maxLength": 256,
- "color": [1, 1, 1, 1],
- "placeholderColor": [0.6, 0.6, 0.6, 1],
- "backgroundColor": [0.15, 0.15, 0.15, 1],
- "borderColor": [0.7, 0.7, 0.7, 1]
+ "color": [122, 156, 198, 1],
+ "placeholderColor": [122, 156, 198, 1],
+ "backgroundColor": [15, 29, 51, 1],
+ "borderColor": [15, 29, 51, 1]
},
{
- "type": "LinearLayout",
- "x": 400,
- "y": 450,
- "width": 1120,
- "height": 80,
- "orientation": "horizontal",
- "spacing": 30,
- "children": [
- {
- "type": "Button",
- "name": "connectButton",
- "x": 0,
- "y": 0,
- "width": 530,
- "height": 80,
- "textures": {
- "normal": "resources/main_menu/single.png",
- "hover": "resources/main_menu/single.png",
- "pressed": "resources/main_menu/single.png"
- }
- },
+ "type": "Button",
+ "name": "connectButton",
+ "x": 449,
+ "y": 350,
+ "width": 382,
+ "height": 56,
+ "textures": {
+ "normal": "resources/multiplayer_menu/Filledbuttons.png",
+ "hover": "resources/multiplayer_menu/Filledbuttons.png",
+ "pressed": "resources/multiplayer_menu/Filledbuttons.png"
+ }
+ },
+
{
"type": "Button",
"name": "backButton",
- "x": 590,
- "y": 0,
- "width": 530,
- "height": 80,
+ "x": 449,
+ "y": 280,
+ "width": 382,
+ "height": 56,
"textures": {
- "normal": "resources/main_menu/exit.png",
- "hover": "resources/main_menu/exit.png",
- "pressed": "resources/main_menu/exit.png"
+ "normal": "resources/multiplayer_menu/Backbutton.png",
+ "hover": "resources/multiplayer_menu/Backbutton.png",
+ "pressed": "resources/multiplayer_menu/Backbutton.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "AvailableServers",
+ "x": 450,
+ "y": 240,
+ "width": 139,
+ "height": 9,
+ "textures": {
+ "normal": "resources/multiplayer_menu/AvailableServers.png",
+ "hover": "resources/multiplayer_menu/AvailableServers.png",
+ "pressed": "resources/multiplayer_menu/AvailableServers.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "SerButton",
+ "x": 436.5,
+ "y": 170,
+ "width": 407,
+ "height": 62,
+ "textures": {
+ "normal": "resources/multiplayer_menu/Button.png",
+ "hover": "resources/multiplayer_menu/Button.png",
+ "pressed": "resources/multiplayer_menu/Button.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "SerButton2",
+ "x": 436.5,
+ "y": 88,
+ "width": 407,
+ "height": 62,
+ "textures": {
+ "normal": "resources/multiplayer_menu/Button2.png",
+ "hover": "resources/multiplayer_menu/Button2.png",
+ "pressed": "resources/multiplayer_menu/Button2.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "SerButton3",
+ "x": 436.5,
+ "y": 6,
+ "width": 407,
+ "height": 62,
+ "textures": {
+ "normal": "resources/multiplayer_menu/Button3.png",
+ "hover": "resources/multiplayer_menu/Button3.png",
+ "pressed": "resources/multiplayer_menu/Button3.png"
+ }
}
- }
]
- },
- {
- "type": "TextView",
- "name": "statusText",
- "x": 400,
- "y": 580,
- "width": 1120,
- "height": 50,
- "text": "Ready to connect",
- "fontPath": "resources/fonts/DroidSans.ttf",
- "fontSize": 24,
- "color": [0.8, 0.8, 0.8, 1],
- "centered": false
- }
- ]
}
}
\ No newline at end of file
diff --git a/resources/config/ship_selection_menu.json b/resources/config/ship_selection_menu.json
new file mode 100644
index 0000000..1a305de
--- /dev/null
+++ b/resources/config/ship_selection_menu.json
@@ -0,0 +1,59 @@
+{
+ "root": {
+ "type": "LinearLayout",
+ "orientation": "vertical",
+ "vertical_align": "center",
+ "horizontal_align": "center",
+ "spacing": 10,
+ "x": 0,
+ "y": 0,
+ "width": "match_parent",
+ "height": "match_parent",
+ "children": [
+ {
+ "type": "LinearLayout",
+ "orientation": "horizontal",
+ "vertical_align": "center",
+ "horizontal_align": "center",
+ "spacing": 10,
+ "width": "match_parent",
+ "height": 260,
+ "children": [
+ {
+ "type": "Button",
+ "name": "spaceshipButton",
+ "width": 256,
+ "height": 256,
+ "textures": {
+ "normal": "resources/multiplayer_menu/ship_fighter.png",
+ "hover": "resources/multiplayer_menu/ship_fighter_pressed.png",
+ "pressed": "resources/multiplayer_menu/ship_fighter_pressed.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "cargoshipButton",
+ "width": 256,
+ "height": 256,
+ "textures": {
+ "normal": "resources/multiplayer_menu/ship_cargo.png",
+ "hover": "resources/multiplayer_menu/ship_cargo_pressed.png",
+ "pressed": "resources/multiplayer_menu/ship_cargo_pressed.png"
+ }
+ }
+ ]
+ },
+ {
+ "type": "Button",
+ "name": "backButton",
+ "width": 382,
+ "height": 56,
+ "textures": {
+ "normal": "resources/multiplayer_menu/Backbutton.png",
+ "hover": "resources/multiplayer_menu/Backbutton.png",
+ "pressed": "resources/multiplayer_menu/Backbutton.png"
+ }
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/resources/config/ui.json b/resources/config/ui.json
index f8f1f7d..fa27292 100644
--- a/resources/config/ui.json
+++ b/resources/config/ui.json
@@ -1,194 +1,57 @@
{
"root": {
- "type": "FrameLayout",
- "x": 0,
- "y": 0,
- "width": 1280,
- "height": 720,
- "children": [
- {
- "type": "FrameLayout",
- "name": "leftPanel",
- "x": 100,
- "y": 100,
- "width": 320,
- "height": 400,
- "children": [
+ "type": "FrameLayout",
+ "x": 0,
+ "y": 0,
+ "width": "match_parent",
+ "height": "match_parent",
+ "children": [
{
- "type": "LinearLayout",
- "name": "mainButtons",
- "orientation": "vertical",
- "spacing": 10,
- "x": 0,
- "y": 0,
- "width": 300,
- "height": 300,
- "children": [
- {
- "type": "Button",
- "name": "playButton",
- "x": -1000,
- "y": 500,
- "width": 200,
- "height": 50,
- "animations": {
- "buttonsExit": {
- "repeat": false,
- "steps": [
- {
- "type": "move",
- "to": [
- -400,
- 0
- ],
- "duration": 1.0,
- "easing": "easein"
- }
- ]
- }
- },
- "textures": {
- "normal": "./resources/sand2.png",
- "hover": "./resources/sand2.png",
- "pressed": "./resources/sand2.png"
- }
- },
- {
- "type": "Button",
- "name": "settingsButton",
- "x": -1000,
- "y": 400,
- "width": 200,
- "height": 50,
- "animations": {
- "buttonsExit": {
- "repeat": false,
- "steps": [
- {
- "type": "wait",
- "duration": 0.5
- },
- {
- "type": "move",
- "to": [
- -400,
- 0
- ],
- "duration": 1.0,
- "easing": "easein"
- }
- ]
- }
- },
- "textures": {
- "normal": "./resources/sand2.png",
- "hover": "./resources/sand2.png",
- "pressed": "./resources/sand2.png"
- }
- },
- {
- "type": "Button",
- "name": "exitButton",
- "x": -1000,
- "y": 300,
- "width": 200,
- "height": 50,
- "animations": {
- "buttonsExit": {
- "repeat": false,
- "steps": [
- {
- "type": "wait",
- "duration": 1.0
- },
- {
- "type": "move",
- "to": [
- -400,
- 0
- ],
- "duration": 1.0,
- "easing": "easein"
- }
- ]
- },
- "bgScroll": {
- "repeat": true,
- "steps": [
- {
- "type": "move",
- "to": [
- 1280,
- 0
- ],
- "duration": 5.0,
- "easing": "linear"
- }
- ]
- }
- },
- "textures": {
- "normal": "./resources/sand2.png",
- "hover": "./resources/sand2.png",
- "pressed": "./resources/sand2.png"
- }
+ "type": "Button",
+ "name": "shootButton",
+ "x": 0,
+ "y": 0,
+ "width": 150,
+ "height": 150,
+ "horizontal_gravity": "right",
+ "vertical_gravity": "bottom",
+ "textures": {
+ "normal": "resources/shoot_normal.png",
+ "hover": "resources/shoot_hover.png",
+ "pressed": "resources/shoot_pressed.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "shootButton2",
+ "x": 0,
+ "y": 0,
+ "width": 150,
+ "height": 150,
+ "horizontal_gravity": "left",
+ "vertical_gravity": "bottom",
+ "textures": {
+ "normal": "resources/shoot_normal.png",
+ "hover": "resources/shoot_hover.png",
+ "pressed": "resources/shoot_pressed.png"
+ }
+ },
+ {
+ "type": "Slider",
+ "name": "velocitySlider",
+ "x": 10,
+ "y": 200,
+ "width": 80,
+ "height": 300,
+ "value": 0.0,
+ "orientation": "vertical",
+ "horizontal_gravity": "right",
+ "vertical_gravity": "bottom",
+ "textures": {
+ "track": "resources/velocitySliderTexture.png",
+ "knob": "resources/velocitySliderButton.png"
}
- ]
}
- ]
- },
- {
- "type": "Slider",
- "name": "velocitySlider",
- "x": 1140,
- "y": 300,
- "width": 50,
- "height": 300,
- "value": 0.0,
- "orientation": "vertical",
- "textures": {
- "track": "resources/velocitySliderTexture.png",
- "knob": "resources/velocitySliderButton.png"
- }
- },
- {
- "type": "Button",
- "name": "shootButton",
- "x": 100,
- "y": 100,
- "width": 100,
- "height": 100,
- "textures": {
- "normal": "resources/shoot_normal.png",
- "hover": "resources/shoot_hover.png",
- "pressed": "resources/shoot_pressed.png"
- }
- },
- {
- "type": "Button",
- "name": "shootButton2",
- "x": 1000,
- "y": 100,
- "width": 100,
- "height": 100,
- "textures": {
- "normal": "resources/shoot_normal.png",
- "hover": "resources/shoot_hover.png",
- "pressed": "resources/shoot_pressed.png"
- }
- },
- {
- "type": "TextView",
- "name": "velocityText",
- "x": 10,
- "y": 10,
- "width": 200,
- "height": 40,
- "text": "Velocity: 0",
- "fontSize": 24,
- "color": [1.0, 1.0, 1.0, 1.0],
- "centered": false
- }
- ]
+ ]
}
- }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/resources/config/ui_old.json b/resources/config/ui_old.json
new file mode 100644
index 0000000..f8f1f7d
--- /dev/null
+++ b/resources/config/ui_old.json
@@ -0,0 +1,194 @@
+{
+ "root": {
+ "type": "FrameLayout",
+ "x": 0,
+ "y": 0,
+ "width": 1280,
+ "height": 720,
+ "children": [
+ {
+ "type": "FrameLayout",
+ "name": "leftPanel",
+ "x": 100,
+ "y": 100,
+ "width": 320,
+ "height": 400,
+ "children": [
+ {
+ "type": "LinearLayout",
+ "name": "mainButtons",
+ "orientation": "vertical",
+ "spacing": 10,
+ "x": 0,
+ "y": 0,
+ "width": 300,
+ "height": 300,
+ "children": [
+ {
+ "type": "Button",
+ "name": "playButton",
+ "x": -1000,
+ "y": 500,
+ "width": 200,
+ "height": 50,
+ "animations": {
+ "buttonsExit": {
+ "repeat": false,
+ "steps": [
+ {
+ "type": "move",
+ "to": [
+ -400,
+ 0
+ ],
+ "duration": 1.0,
+ "easing": "easein"
+ }
+ ]
+ }
+ },
+ "textures": {
+ "normal": "./resources/sand2.png",
+ "hover": "./resources/sand2.png",
+ "pressed": "./resources/sand2.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "settingsButton",
+ "x": -1000,
+ "y": 400,
+ "width": 200,
+ "height": 50,
+ "animations": {
+ "buttonsExit": {
+ "repeat": false,
+ "steps": [
+ {
+ "type": "wait",
+ "duration": 0.5
+ },
+ {
+ "type": "move",
+ "to": [
+ -400,
+ 0
+ ],
+ "duration": 1.0,
+ "easing": "easein"
+ }
+ ]
+ }
+ },
+ "textures": {
+ "normal": "./resources/sand2.png",
+ "hover": "./resources/sand2.png",
+ "pressed": "./resources/sand2.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "exitButton",
+ "x": -1000,
+ "y": 300,
+ "width": 200,
+ "height": 50,
+ "animations": {
+ "buttonsExit": {
+ "repeat": false,
+ "steps": [
+ {
+ "type": "wait",
+ "duration": 1.0
+ },
+ {
+ "type": "move",
+ "to": [
+ -400,
+ 0
+ ],
+ "duration": 1.0,
+ "easing": "easein"
+ }
+ ]
+ },
+ "bgScroll": {
+ "repeat": true,
+ "steps": [
+ {
+ "type": "move",
+ "to": [
+ 1280,
+ 0
+ ],
+ "duration": 5.0,
+ "easing": "linear"
+ }
+ ]
+ }
+ },
+ "textures": {
+ "normal": "./resources/sand2.png",
+ "hover": "./resources/sand2.png",
+ "pressed": "./resources/sand2.png"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "Slider",
+ "name": "velocitySlider",
+ "x": 1140,
+ "y": 300,
+ "width": 50,
+ "height": 300,
+ "value": 0.0,
+ "orientation": "vertical",
+ "textures": {
+ "track": "resources/velocitySliderTexture.png",
+ "knob": "resources/velocitySliderButton.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "shootButton",
+ "x": 100,
+ "y": 100,
+ "width": 100,
+ "height": 100,
+ "textures": {
+ "normal": "resources/shoot_normal.png",
+ "hover": "resources/shoot_hover.png",
+ "pressed": "resources/shoot_pressed.png"
+ }
+ },
+ {
+ "type": "Button",
+ "name": "shootButton2",
+ "x": 1000,
+ "y": 100,
+ "width": 100,
+ "height": 100,
+ "textures": {
+ "normal": "resources/shoot_normal.png",
+ "hover": "resources/shoot_hover.png",
+ "pressed": "resources/shoot_pressed.png"
+ }
+ },
+ {
+ "type": "TextView",
+ "name": "velocityText",
+ "x": 10,
+ "y": 10,
+ "width": 200,
+ "height": 40,
+ "text": "Velocity: 0",
+ "fontSize": 24,
+ "color": [1.0, 1.0, 1.0, 1.0],
+ "centered": false
+ }
+ ]
+ }
+ }
\ No newline at end of file
diff --git a/resources/game_over/Container.png b/resources/game_over/Container.png
new file mode 100644
index 0000000..e2af5b9
--- /dev/null
+++ b/resources/game_over/Container.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4292ab255136aeeff003e265bfde42bef4aabb092427bd68f9b2ac42d86916a1
+size 27198
diff --git a/resources/game_over/Filledbuttons.png b/resources/game_over/Filledbuttons.png
new file mode 100644
index 0000000..a9b2806
--- /dev/null
+++ b/resources/game_over/Filledbuttons.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:436d9137c479b475d8bc753961986ea3f58b6a2439de6f83b5d707172c3f2ff9
+size 7976
diff --git a/resources/game_over/FinalScore.png b/resources/game_over/FinalScore.png
new file mode 100644
index 0000000..af06f07
--- /dev/null
+++ b/resources/game_over/FinalScore.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ea4e2a8408fa1b68793fd8d81135902ff15ef2779ea898a99a9ef83ff6e147e8
+size 4142
diff --git a/resources/game_over/MissionFailed.png b/resources/game_over/MissionFailed.png
new file mode 100644
index 0000000..ec8d6c3
--- /dev/null
+++ b/resources/game_over/MissionFailed.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c2187818c1fbfb127f70c130f033aa7c16cc3b3a02a2ea59317413437f530c30
+size 9982
diff --git a/resources/game_over/Secondarybutton.png b/resources/game_over/Secondarybutton.png
new file mode 100644
index 0000000..37cdae0
--- /dev/null
+++ b/resources/game_over/Secondarybutton.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:113c524330190fbdf36f0f7b4ebfc03032170aff5011f8c17201533b52db872f
+size 5387
diff --git a/resources/multiplayer_menu/AvailableServers.png b/resources/multiplayer_menu/AvailableServers.png
new file mode 100644
index 0000000..d4c055e
--- /dev/null
+++ b/resources/multiplayer_menu/AvailableServers.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:69624bbb22ebac925c3eb9299c862f07fa0815a6d90ed14e7e91a14be588e4d0
+size 8167
diff --git a/resources/multiplayer_menu/Backbutton.png b/resources/multiplayer_menu/Backbutton.png
new file mode 100644
index 0000000..37cdae0
--- /dev/null
+++ b/resources/multiplayer_menu/Backbutton.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:113c524330190fbdf36f0f7b4ebfc03032170aff5011f8c17201533b52db872f
+size 5387
diff --git a/resources/multiplayer_menu/Button.png b/resources/multiplayer_menu/Button.png
new file mode 100644
index 0000000..620425a
--- /dev/null
+++ b/resources/multiplayer_menu/Button.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2e0267f247715722ee0881e96dc83d1c83a999c718af1bac64b5964c1a2c7335
+size 8074
diff --git a/resources/multiplayer_menu/Button2.png b/resources/multiplayer_menu/Button2.png
new file mode 100644
index 0000000..884b2fa
--- /dev/null
+++ b/resources/multiplayer_menu/Button2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:629aa9499976abeeb83194b4cc67153383ab4ebdfc46dd98e4c3733ee7b92e4e
+size 8612
diff --git a/resources/multiplayer_menu/Button3.png b/resources/multiplayer_menu/Button3.png
new file mode 100644
index 0000000..892fcca
--- /dev/null
+++ b/resources/multiplayer_menu/Button3.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a74a8b4c954ceb45d582b46d0e5bf2f544f5d933d1b264b4d80fb2720c3a6e68
+size 9129
diff --git a/resources/multiplayer_menu/Filledbuttons.png b/resources/multiplayer_menu/Filledbuttons.png
new file mode 100644
index 0000000..26f29f7
--- /dev/null
+++ b/resources/multiplayer_menu/Filledbuttons.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8977b432e9b190f568456026e05fc9ae3a9eec7a90b285095578071c03e839ea
+size 8375
diff --git a/resources/multiplayer_menu/JoinServer.png b/resources/multiplayer_menu/JoinServer.png
new file mode 100644
index 0000000..7db6904
--- /dev/null
+++ b/resources/multiplayer_menu/JoinServer.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2f01d975f7e6bec5e796b8b4272462b0b4d3d99076af3873c4d819a50b9dbe82
+size 4177
diff --git a/resources/multiplayer_menu/ServerName.png b/resources/multiplayer_menu/ServerName.png
new file mode 100644
index 0000000..d8df64c
--- /dev/null
+++ b/resources/multiplayer_menu/ServerName.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7b197ead1094f297c06083c909d8a910812d0981386578a75fd74af67d54819e
+size 4304
diff --git a/resources/multiplayer_menu/ship_cargo.png b/resources/multiplayer_menu/ship_cargo.png
new file mode 100644
index 0000000..88a6f68
--- /dev/null
+++ b/resources/multiplayer_menu/ship_cargo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c86eb962f4abf04aacf7ebbf07433611e9afed42b0f7806fc892ae4a81b45b58
+size 13296
diff --git a/resources/multiplayer_menu/ship_cargo_pressed.png b/resources/multiplayer_menu/ship_cargo_pressed.png
new file mode 100644
index 0000000..30590cf
--- /dev/null
+++ b/resources/multiplayer_menu/ship_cargo_pressed.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:97d66a6cd037fd75d6f78e72d37b14362683a0de38557470806db53d5e5675d9
+size 13694
diff --git a/resources/multiplayer_menu/ship_fighter.png b/resources/multiplayer_menu/ship_fighter.png
new file mode 100644
index 0000000..4c6ea5f
--- /dev/null
+++ b/resources/multiplayer_menu/ship_fighter.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:072bf292acb2760e1ce0ab6c783eca114614d95a9451ba3c86088eee9d824b43
+size 29780
diff --git a/resources/multiplayer_menu/ship_fighter_pressed.png b/resources/multiplayer_menu/ship_fighter_pressed.png
new file mode 100644
index 0000000..c0628eb
--- /dev/null
+++ b/resources/multiplayer_menu/ship_fighter_pressed.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d7a120a716261b122f6a90a04d7a9bbf1c9582e8c9c114e633bc348c15f7d002
+size 29926
diff --git a/resources/multiplayer_menu/title.png b/resources/multiplayer_menu/title.png
new file mode 100644
index 0000000..30e6644
--- /dev/null
+++ b/resources/multiplayer_menu/title.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d52df5a072c43ef1a113cdc6fcb0af32fe5fcf353775bb0ad6907921ced30840
+size 7477
diff --git a/resources/spark2.png b/resources/spark2.png
new file mode 100644
index 0000000..b0e806f
--- /dev/null
+++ b/resources/spark2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:11cde0c85c95f91eb9ace65cead22a37ba80d215897f32a2d6be1410210c1acf
+size 2656
diff --git a/server/server.cpp b/server/server.cpp
index 301823c..35467ef 100644
--- a/server/server.cpp
+++ b/server/server.cpp
@@ -53,7 +53,7 @@ struct Projectile {
uint64_t spawnMs = 0;
Eigen::Vector3f pos;
Eigen::Vector3f vel;
- float lifeMs = 5000.0f;
+ float lifeMs = PROJECTILE_LIFE;
};
struct BoxDestroyedInfo {
@@ -93,6 +93,9 @@ class Session : public std::enable_shared_from_this {
public:
ClientStateInterval timedClientStates;
+ std::string nickname = "Player";
+ int shipType = 0;
+
explicit Session(tcp::socket&& socket, int id)
: ws_(std::move(socket)), id_(id) {
}
@@ -157,17 +160,19 @@ private:
std::lock_guard lock(g_boxes_mutex);
std::string boxMsg = "BOXES:";
- for (const auto& box : g_serverBoxes) {
+ for (size_t i = 0; i < g_serverBoxes.size(); ++i) {
+ const auto& box = g_serverBoxes[i];
Eigen::Quaternionf q(box.rotation);
- boxMsg += std::to_string(box.position.x()) + ":" +
+ boxMsg += std::to_string(i) + ":" +
+ std::to_string(box.position.x()) + ":" +
std::to_string(box.position.y()) + ":" +
std::to_string(box.position.z()) + ":" +
std::to_string(q.w()) + ":" +
std::to_string(q.x()) + ":" +
std::to_string(q.y()) + ":" +
- std::to_string(q.z()) + "|";
+ std::to_string(q.z()) + ":" +
+ (std::to_string(box.destroyed ? 1 : 0)) + "|";
}
-
if (!boxMsg.empty() && boxMsg.back() == '|') boxMsg.pop_back();
send_message(boxMsg);
@@ -205,7 +210,7 @@ public:
return latest;
}
-
+
void doWrite() {
std::lock_guard lock(writeMutex_);
if (is_writing_ || writeQueue_.empty()) {
@@ -253,7 +258,6 @@ private:
void process_message(const std::string& msg) {
if (!IsMessageValid(msg)) {
- // Логируем попытку подмены и просто выходим из обработки
std::cout << "[Security] Invalid packet hash. Dropping message: " << msg << std::endl;
return;
}
@@ -266,7 +270,40 @@ private:
std::string type = parts[0];
- if (type == "UPD") {
+ if (type == "JOIN") {
+ std::string nick = "Player";
+ int sType = 0;
+ if (parts.size() >= 2) nick = parts[1];
+ if (parts.size() >= 3) {
+ try { sType = std::stoi(parts[2]); }
+ catch (...) { sType = 0; }
+ }
+
+ this->nickname = nick;
+ this->shipType = sType;
+
+ std::cout << "Server: Player " << id_ << " joined as [" << nick << "] shipType=" << sType << std::endl;
+
+ std::string info = "PLAYERINFO:" + std::to_string(id_) + ":" + nick + ":" + std::to_string(sType);
+
+ {
+ std::lock_guard lock(g_sessions_mutex);
+ for (auto& session : g_sessions) {
+ if (session->get_id() == this->id_) continue;
+ session->send_message(info);
+ }
+ }
+ {
+ std::lock_guard lock(g_sessions_mutex);
+ for (auto& session : g_sessions) {
+ if (session->get_id() == this->id_) continue;
+ std::string otherInfo = "PLAYERINFO:" + std::to_string(session->get_id()) + ":" + session->nickname + ":" + std::to_string(session->shipType);
+ // Отправляем именно новому клиенту
+ this->send_message(otherInfo);
+ }
+ }
+ }
+ else if (type == "UPD") {
{
std::lock_guard gd(g_dead_mutex);
if (g_dead_players.find(id_) != g_dead_players.end()) {
@@ -284,20 +321,47 @@ private:
};
receivedState.lastUpdateServerTime = uptime_timepoint;
receivedState.handle_full_sync(parts, 2);
+ receivedState.nickname = this->nickname;
+ receivedState.shipType = this->shipType;
timedClientStates.add_state(receivedState);
- retranslateMessage(cleanMessage);
}
- else if (parts[0] == "RESPAWN") {
+ else if (type == "RESPAWN") {
{
std::lock_guard gd(g_dead_mutex);
g_dead_players.erase(id_);
}
- std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_);
- broadcastToAll(respawnMsg);
+ {
+ auto now_tp = std::chrono::system_clock::now();
+ uint64_t now_ms = static_cast(std::chrono::duration_cast(now_tp.time_since_epoch()).count());
- std::cout << "Server: Player " << id_ << " respawned\n";
+ ClientState st;
+ st.id = id_;
+ st.position = Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
+ st.rotation = Eigen::Matrix3f::Identity();
+ st.currentAngularVelocity = Eigen::Vector3f::Zero();
+ st.velocity = 0.0f;
+ st.selectedVelocity = 0;
+ st.discreteMag = 0.0f;
+ st.discreteAngle = -1;
+ st.lastUpdateServerTime = now_tp;
+ st.nickname = this->nickname;
+ st.shipType = this->shipType;
+
+ timedClientStates.add_state(st);
+
+ std::string respawnMsg = "RESPAWN_ACK:" + std::to_string(id_);
+ broadcastToAll(respawnMsg);
+
+ std::string playerInfo = "PLAYERINFO:" + std::to_string(id_) + ":" + st.nickname + ":" + std::to_string(st.shipType);
+ broadcastToAll(playerInfo);
+
+ std::string eventMsg = "EVENT:" + std::to_string(id_) + ":UPD:" + std::to_string(now_ms) + ":" + st.formPingMessageContent();
+ broadcastToAll(eventMsg);
+
+ std::cout << "Server: Player " << id_ << " respawned, broadcasted RESPAWN_ACK, PLAYERINFO and initial UPD\n";
+ }
}
else if (parts[0] == "FIRE") {
if (parts.size() < 10) return;
@@ -344,8 +408,8 @@ private:
Eigen::Vector3f(1.5f, 0.9f - 6.f, 5.0f)
};
- uint64_t now_ms = std::chrono::duration_cast(
- std::chrono::system_clock::now().time_since_epoch()).count();
+ uint64_t now_ms = std::chrono::duration_cast((
+ std::chrono::system_clock::now().time_since_epoch())).count();
std::lock_guard pl(g_projectiles_mutex);
for (int i = 0; i < std::min(shotCount, (int)localOffsets.size()); ++i) {
@@ -370,16 +434,6 @@ private:
}
}
- void retranslateMessage(const std::string& msg) {
- std::string event_msg = "EVENT:" + std::to_string(id_) + ":" + msg;
-
- std::lock_guard lock(g_sessions_mutex);
- for (auto& session : g_sessions) {
- if (session->get_id() != id_) {
- session->send_message(event_msg);
- }
- }
- }
};
void broadcastToAll(const std::string& message) {
@@ -391,273 +445,255 @@ void broadcastToAll(const std::string& message) {
void update_world(net::steady_timer& timer, net::io_context& ioc) {
- static auto last_snapshot_time = std::chrono::steady_clock::now();
- auto now = std::chrono::steady_clock::now();
- /*static uint64_t lastTickCount = 0;
+ static auto last_snapshot_time = std::chrono::system_clock::now();
- if (lastTickCount == 0) {
- //lastTickCount = SDL_GetTicks64();
- lastTickCount = std::chrono::duration_cast(
- std::chrono::system_clock::now().time_since_epoch()
- ).count();
+ auto now = std::chrono::system_clock::now();
+ uint64_t now_ms = static_cast(
+ std::chrono::duration_cast(now.time_since_epoch()).count());
- lastTickCount = (lastTickCount / 50) * 50;
-
- return;
- }
-
-
- auto newTickCount = std::chrono::duration_cast(
- std::chrono::system_clock::now().time_since_epoch()
- ).count();
-
- newTickCount = (newTickCount / 50) * 50;
-
- int64_t deltaMs = static_cast(newTickCount - lastTickCount);
-
- std::chrono::system_clock::time_point nowRounded = std::chrono::system_clock::time_point(std::chrono::milliseconds(newTickCount));
- */
- // For each player
- // Get letest state + add time (until newTickCount)
- // Calculate if collisions with boxes
-
-
-
-
- // Рассылка Snapshot раз в 1000мс
- /*
- if (std::chrono::duration_cast(now - last_snapshot_time).count() >= 1000) {
+ // --- Snapshot every 500ms ---
+ /*if (std::chrono::duration_cast(now - last_snapshot_time).count() >= 500) {
last_snapshot_time = now;
- auto system_now = std::chrono::system_clock::now();
-
- std::string snapshot_msg = "SNAPSHOT:" + std::to_string(
- std::chrono::duration_cast(
- system_now.time_since_epoch()).count()
- );
+ std::string snapshot_msg = "SNAPSHOT:" + std::to_string(now_ms);
std::lock_guard lock(g_sessions_mutex);
-
- // Формируем общую строку состояний всех игроков
for (auto& session : g_sessions) {
- ClientState st = session->get_latest_state(system_now);
+ ClientState st = session->get_latest_state(now);
snapshot_msg += "|" + std::to_string(session->get_id()) + ":" + st.formPingMessageContent();
}
-
for (auto& session : g_sessions) {
session->send_message(snapshot_msg);
}
}*/
- const std::chrono::milliseconds interval(50);
- timer.expires_after(interval);
+ // --- Tick: broadcast each player's latest state to all others (20Hz) ---
+ // Send the raw last-known state with its original timestamp, NOT an extrapolated one.
+ // Extrapolating here causes snap-back: if a player stops rotating, the server would
+ // keep sending over-rotated positions until the new state arrives, then B snaps back.
+ {
+ std::lock_guard lock(g_sessions_mutex);
+ for (auto& sender : g_sessions) {
+ if (sender->timedClientStates.timedStates.empty()) continue;
- timer.async_wait([&](const boost::system::error_code& ec) {
- if (ec) return;
+ const ClientState& st = sender->timedClientStates.timedStates.back();
+ uint64_t stateTime = static_cast(
+ std::chrono::duration_cast(
+ st.lastUpdateServerTime.time_since_epoch()).count());
- auto now = std::chrono::system_clock::now();
- uint64_t now_ms = static_cast(std::chrono::duration_cast(now.time_since_epoch()).count());
+ std::string event_msg = "EVENT:" + std::to_string(sender->get_id()) +
+ ":UPD:" + std::to_string(stateTime) + ":" + st.formPingMessageContent();
- std::vector deathEvents;
-
- {
- std::lock_guard pl(g_projectiles_mutex);
- std::vector indicesToRemove;
-
- float dt = 50.0f / 1000.0f;
-
- for (size_t i = 0; i < g_projectiles.size(); ++i) {
- auto& pr = g_projectiles[i];
-
- pr.pos += pr.vel * dt;
-
- if (now_ms > pr.spawnMs + static_cast(pr.lifeMs)) {
- indicesToRemove.push_back(static_cast(i));
- continue;
- }
-
- bool hitDetected = false;
-
- {
- std::lock_guard lm(g_sessions_mutex);
- std::lock_guard gd(g_dead_mutex);
-
- for (auto& session : g_sessions) {
- int targetId = session->get_id();
-
- if (targetId == pr.shooterId) continue;
- if (g_dead_players.find(targetId) != g_dead_players.end()) continue;
-
- ClientState targetState;
- if (!session->fetchStateAtTime(now, targetState)) continue;
-
- Eigen::Vector3f diff = pr.pos - targetState.position;
- const float shipRadius = 15.0f;
- const float projectileRadius = 1.5f;
- float combinedRadius = shipRadius + projectileRadius;
-
- if (diff.squaredNorm() <= combinedRadius * combinedRadius) {
- DeathInfo death;
- death.targetId = targetId;
- death.serverTime = now_ms;
- death.position = pr.pos;
- death.killerId = pr.shooterId;
-
- deathEvents.push_back(death);
- g_dead_players.insert(targetId);
- indicesToRemove.push_back(static_cast(i));
- hitDetected = true;
-
- std::cout << "Server: *** HIT DETECTED! ***" << std::endl;
- std::cout << "Server: Projectile at ("
- << pr.pos.x() << ", " << pr.pos.y() << ", " << pr.pos.z()
- << ") hit player " << targetId << std::endl;
- break;
- }
- }
- }
-
- if (hitDetected) continue;
- }
-
- if (!indicesToRemove.empty()) {
- std::sort(indicesToRemove.rbegin(), indicesToRemove.rend());
- for (int idx : indicesToRemove) {
- if (idx >= 0 && idx < (int)g_projectiles.size()) {
- g_projectiles.erase(g_projectiles.begin() + idx);
- }
+ for (auto& receiver : g_sessions) {
+ if (receiver->get_id() != sender->get_id()) {
+ receiver->send_message(event_msg);
}
}
}
+ }
- {
- std::lock_guard bm(g_boxes_mutex);
- //const float projectileHitRadius = 1.5f;
- const float projectileHitRadius = 5.0f;
- const float boxCollisionRadius = 2.0f;
+ // --- Tick: projectile movement and hit detection ---
+ const float dt = 50.0f / 1000.0f;
+ std::vector deathEvents;
- std::vector> boxProjectileCollisions;
+ {
+ std::lock_guard pl(g_projectiles_mutex);
+ std::vector indicesToRemove;
- for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
- if (g_serverBoxes[bi].destroyed) continue;
+ for (size_t i = 0; i < g_projectiles.size(); ++i) {
+ auto& pr = g_projectiles[i];
- Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
+ pr.pos += pr.vel * dt;
- for (size_t pi = 0; pi < g_projectiles.size(); ++pi) {
- const auto& pr = g_projectiles[pi];
- Eigen::Vector3f diff = pr.pos - boxWorld;
- //std::cout << "diff norm is " << diff.norm() << std::endl;
- float thresh = boxCollisionRadius + projectileHitRadius;
-
- if (diff.squaredNorm() <= thresh * thresh) {
- boxProjectileCollisions.push_back({ bi, pi });
- }
- }
+ if (now_ms > pr.spawnMs + static_cast(pr.lifeMs)) {
+ indicesToRemove.push_back(static_cast(i));
+ continue;
}
- for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) {
- g_serverBoxes[boxIdx].destroyed = true;
+ bool hitDetected = false;
- Eigen::Vector3f boxWorld = g_serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
-
- BoxDestroyedInfo destruction;
- destruction.boxIndex = static_cast(boxIdx);
- destruction.serverTime = now_ms;
- destruction.position = boxWorld;
- destruction.destroyedBy = g_projectiles[projIdx].shooterId;
-
- {
- std::lock_guard dm(g_boxDestructions_mutex);
- g_boxDestructions.push_back(destruction);
- }
-
- std::cout << "Server: Box " << boxIdx << " destroyed by projectile from player "
- << g_projectiles[projIdx].shooterId << std::endl;
- }
- }
-
- {
- std::lock_guard bm(g_boxes_mutex);
- std::lock_guard lm(g_sessions_mutex);
-
- const float shipCollisionRadius = 15.0f;
- const float boxCollisionRadius = 2.0f;
-
- for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
- if (g_serverBoxes[bi].destroyed) continue;
-
- Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
+ {
+ std::lock_guard lm(g_sessions_mutex);
+ std::lock_guard gd(g_dead_mutex);
for (auto& session : g_sessions) {
- {
- std::lock_guard gd(g_dead_mutex);
- if (g_dead_players.find(session->get_id()) != g_dead_players.end()) {
- continue;
- }
- }
+ int targetId = session->get_id();
- ClientState shipState;
- if (!session->fetchStateAtTime(now, shipState)) continue;
+ if (targetId == pr.shooterId) continue;
+ if (g_dead_players.find(targetId) != g_dead_players.end()) continue;
- Eigen::Vector3f diff = shipState.position - boxWorld;
- float thresh = shipCollisionRadius + boxCollisionRadius;
+ ClientState targetState;
+ if (!session->fetchStateAtTime(now, targetState)) continue;
- if (diff.squaredNorm() <= thresh * thresh) {
- g_serverBoxes[bi].destroyed = true;
+ Eigen::Vector3f diff = pr.pos - targetState.position;
+ const float shipRadius = 15.0f;
+ const float projectileRadius = 1.5f;
+ float combinedRadius = shipRadius + projectileRadius;
- BoxDestroyedInfo destruction;
- destruction.boxIndex = static_cast(bi);
- destruction.serverTime = now_ms;
- destruction.position = boxWorld;
- destruction.destroyedBy = session->get_id();
+ if (diff.squaredNorm() <= combinedRadius * combinedRadius) {
+ DeathInfo death;
+ death.targetId = targetId;
+ death.serverTime = now_ms;
+ death.position = pr.pos;
+ death.killerId = pr.shooterId;
- {
- std::lock_guard dm(g_boxDestructions_mutex);
- g_boxDestructions.push_back(destruction);
- }
+ deathEvents.push_back(death);
+ g_dead_players.insert(targetId);
+ indicesToRemove.push_back(static_cast(i));
+ hitDetected = true;
- std::cout << "Server: Box " << bi << " destroyed by ship collision with player "
- << session->get_id() << std::endl;
+ std::cout << "Server: *** HIT DETECTED! ***" << std::endl;
+ std::cout << "Server: Projectile at ("
+ << pr.pos.x() << ", " << pr.pos.y() << ", " << pr.pos.z()
+ << ") hit player " << targetId << std::endl;
break;
}
}
}
+
+ if (hitDetected) continue;
}
- if (!deathEvents.empty()) {
- for (const auto& death : deathEvents) {
- std::string deadMsg = "DEAD:" +
- std::to_string(death.serverTime) + ":" +
- std::to_string(death.targetId) + ":" +
- std::to_string(death.position.x()) + ":" +
- std::to_string(death.position.y()) + ":" +
- std::to_string(death.position.z()) + ":" +
- std::to_string(death.killerId);
+ if (!indicesToRemove.empty()) {
+ std::sort(indicesToRemove.rbegin(), indicesToRemove.rend());
+ for (int idx : indicesToRemove) {
+ if (idx >= 0 && idx < (int)g_projectiles.size()) {
+ g_projectiles.erase(g_projectiles.begin() + idx);
+ }
+ }
+ }
+ }
- broadcastToAll(deadMsg);
- std::cout << "Server: Sent DEAD event - Player " << death.targetId
- << " killed by " << death.killerId << std::endl;
+
+ // --- Tick: box-projectile collisions ---
+ {
+ std::lock_guard bm(g_boxes_mutex);
+
+
+ std::vector> boxProjectileCollisions;
+
+ for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
+ if (g_serverBoxes[bi].destroyed) continue;
+
+ Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
+
+ for (size_t pi = 0; pi < g_projectiles.size(); ++pi) {
+ const auto& pr = g_projectiles[pi];
+ Eigen::Vector3f diff = pr.pos - boxWorld;
+ float thresh = boxCollisionRadius + projectileHitRadius;
+
+ if (diff.squaredNorm() <= thresh * thresh) {
+ boxProjectileCollisions.push_back({ bi, pi });
+ }
}
}
- {
- std::lock_guard dm(g_boxDestructions_mutex);
- for (const auto& destruction : g_boxDestructions) {
- std::string boxMsg = "BOX_DESTROYED:" +
- std::to_string(destruction.boxIndex) + ":" +
- std::to_string(destruction.serverTime) + ":" +
- std::to_string(destruction.position.x()) + ":" +
- std::to_string(destruction.position.y()) + ":" +
- std::to_string(destruction.position.z()) + ":" +
- std::to_string(destruction.destroyedBy);
+ for (const auto& [boxIdx, projIdx] : boxProjectileCollisions) {
+ g_serverBoxes[boxIdx].destroyed = true;
- broadcastToAll(boxMsg);
- std::cout << "Server: Broadcasted BOX_DESTROYED for box " << destruction.boxIndex << std::endl;
+ Eigen::Vector3f boxWorld = g_serverBoxes[boxIdx].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
+
+ BoxDestroyedInfo destruction;
+ destruction.boxIndex = static_cast(boxIdx);
+ destruction.serverTime = now_ms;
+ destruction.position = boxWorld;
+ destruction.destroyedBy = g_projectiles[projIdx].shooterId;
+
+ {
+ std::lock_guard dm(g_boxDestructions_mutex);
+ g_boxDestructions.push_back(destruction);
}
- g_boxDestructions.clear();
- }
+ std::cout << "Server: Box " << boxIdx << " destroyed by projectile from player "
+ << g_projectiles[projIdx].shooterId << std::endl;
+ }
+ }
+
+ // --- Tick: box-ship collisions ---
+ {
+ std::lock_guard bm(g_boxes_mutex);
+ std::lock_guard lm(g_sessions_mutex);
+
+ for (size_t bi = 0; bi < g_serverBoxes.size(); ++bi) {
+ if (g_serverBoxes[bi].destroyed) continue;
+
+ Eigen::Vector3f boxWorld = g_serverBoxes[bi].position + Eigen::Vector3f(0.0f, 0.0f, 45000.0f);
+
+ for (auto& session : g_sessions) {
+ {
+ std::lock_guard gd(g_dead_mutex);
+ if (g_dead_players.find(session->get_id()) != g_dead_players.end()) {
+ continue;
+ }
+ }
+
+ ClientState shipState;
+ if (!session->fetchStateAtTime(now, shipState)) continue;
+
+ Eigen::Vector3f diff = shipState.position - boxWorld;
+ float thresh = shipCollisionRadius + boxCollisionRadius;
+
+ if (diff.squaredNorm() <= thresh * thresh) {
+ g_serverBoxes[bi].destroyed = true;
+
+ BoxDestroyedInfo destruction;
+ destruction.boxIndex = static_cast(bi);
+ destruction.serverTime = now_ms;
+ destruction.position = boxWorld;
+ destruction.destroyedBy = session->get_id();
+
+ {
+ std::lock_guard dm(g_boxDestructions_mutex);
+ g_boxDestructions.push_back(destruction);
+ }
+
+ std::cout << "Server: Box " << bi << " destroyed by ship collision with player "
+ << session->get_id() << std::endl;
+ break;
+ }
+ }
+ }
+ }
+
+ // --- Broadcast deaths ---
+ for (const auto& death : deathEvents) {
+ std::string deadMsg = "DEAD:" +
+ std::to_string(death.serverTime) + ":" +
+ std::to_string(death.targetId) + ":" +
+ std::to_string(death.position.x()) + ":" +
+ std::to_string(death.position.y()) + ":" +
+ std::to_string(death.position.z()) + ":" +
+ std::to_string(death.killerId);
+
+ broadcastToAll(deadMsg);
+
+ std::cout << "Server: Sent DEAD event - Player " << death.targetId
+ << " killed by " << death.killerId << std::endl;
+ }
+
+ // --- Broadcast box destructions ---
+ {
+ std::lock_guard dm(g_boxDestructions_mutex);
+ for (const auto& destruction : g_boxDestructions) {
+ std::string boxMsg = "BOX_DESTROYED:" +
+ std::to_string(destruction.boxIndex) + ":" +
+ std::to_string(destruction.serverTime) + ":" +
+ std::to_string(destruction.position.x()) + ":" +
+ std::to_string(destruction.position.y()) + ":" +
+ std::to_string(destruction.position.z()) + ":" +
+ std::to_string(destruction.destroyedBy);
+
+ broadcastToAll(boxMsg);
+ std::cout << "Server: Broadcasted BOX_DESTROYED for box " << destruction.boxIndex << std::endl;
+ }
+ g_boxDestructions.clear();
+ }
+
+ // --- Schedule next tick in 50ms ---
+ timer.expires_after(std::chrono::milliseconds(50));
+ timer.async_wait([&timer, &ioc](const boost::system::error_code& ec) {
+ if (ec) return;
update_world(timer, ioc);
});
}
@@ -667,8 +703,8 @@ std::vector generateServerBoxes(int count) {
std::random_device rd;
std::mt19937 gen(rd());
- const float MIN_COORD = -100.0f;
- const float MAX_COORD = 100.0f;
+ const float MIN_COORD = -1000.0f;
+ const float MAX_COORD = 1000.0f;
const float MIN_DISTANCE = 3.0f;
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
const int MAX_ATTEMPTS = 1000;
diff --git a/src/Environment.cpp b/src/Environment.cpp
index 1fec925..9c41b4d 100644
--- a/src/Environment.cpp
+++ b/src/Environment.cpp
@@ -36,5 +36,25 @@ ClientState Environment::shipState;
const float Environment::CONST_Z_NEAR = 5.f;
const float Environment::CONST_Z_FAR = 5000.f;
+float Environment::projectionWidth = 1280.0f;
+float Environment::projectionHeight = 720.0f;
+
+void Environment::computeProjectionDimensions()
+{
+ if (width <= 0 || height <= 0) return;
+
+ const float refShortSide = 720.0f;
+ float aspect = (float)width / (float)height;
+
+ if (width >= height) {
+ // Landscape: fix height to 720, scale width to preserve aspect
+ projectionHeight = refShortSide;
+ projectionWidth = refShortSide * aspect;
+ } else {
+ // Portrait: fix width to 720, scale height to preserve aspect
+ projectionWidth = refShortSide;
+ projectionHeight = refShortSide / aspect;
+ }
+}
} // namespace ZL
diff --git a/src/Environment.h b/src/Environment.h
index 5d3c1ca..17125ac 100644
--- a/src/Environment.h
+++ b/src/Environment.h
@@ -35,8 +35,15 @@ public:
static const float CONST_Z_NEAR;
static const float CONST_Z_FAR;
+ // Virtual projection dimensions used for all 2D/UI rendering.
+ // These maintain the screen's actual aspect ratio but normalize the
+ // height to 720 (landscape) or width to 720 (portrait), giving a
+ // consistent coordinate space regardless of physical screen resolution.
+ static float projectionWidth;
+ static float projectionHeight;
-
+ // Call this once at startup and whenever the window is resized.
+ static void computeProjectionDimensions();
};
} // namespace ZL
diff --git a/src/Game.cpp b/src/Game.cpp
index d6c28c0..210d181 100644
--- a/src/Game.cpp
+++ b/src/Game.cpp
@@ -23,7 +23,12 @@
#endif
#endif
+#ifdef EMSCRIPTEN
+#include
+#endif
+
#include "network/LocalClient.h"
+#include "network/ClientState.h"
namespace ZL
@@ -35,6 +40,22 @@ namespace ZL
const char* CONST_ZIP_FILE = "";
#endif
+#ifdef EMSCRIPTEN
+ Game* Game::s_instance = nullptr;
+
+ void Game::onResourcesZipLoaded(const char* /*filename*/) {
+ if (s_instance) {
+ s_instance->mainThreadHandler.EnqueueMainThreadTask([&]() {
+ s_instance->setupPart2();
+ });
+ }
+ }
+
+ void Game::onResourcesZipError(const char* /*filename*/) {
+ std::cerr << "Failed to download resources.zip" << std::endl;
+ }
+#endif
+
Game::Game()
: window(nullptr)
, glContext(nullptr)
@@ -52,32 +73,47 @@ namespace ZL
if (window) {
SDL_DestroyWindow(window);
}
+#ifndef EMSCRIPTEN
+ // In Emscripten, SDL must stay alive across context loss/restore cycles
+ // so the window remains valid when the game object is re-created.
SDL_Quit();
+#endif
}
void Game::setup() {
glContext = SDL_GL_CreateContext(ZL::Environment::window);
+ Environment::computeProjectionDimensions();
+
ZL::BindOpenGlFunctions();
ZL::CheckGlError();
renderer.InitOpenGL();
#ifdef EMSCRIPTEN
- renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", CONST_ZIP_FILE);
- renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", CONST_ZIP_FILE);
-
+ // These shaders and loading.png are preloaded separately (not from zip),
+ // so they are available immediately without waiting for resources.zip.
+ renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_web.fragment", "");
+ renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_web.fragment", "");
+ loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", ""));
#else
renderer.shaderManager.AddShaderFromFiles("defaultColor", "resources/shaders/defaultColor.vertex", "resources/shaders/defaultColor_desktop.fragment", CONST_ZIP_FILE);
renderer.shaderManager.AddShaderFromFiles("default", "resources/shaders/default.vertex", "resources/shaders/default_desktop.fragment", CONST_ZIP_FILE);
+ loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
#endif
- loadingTexture = std::make_unique(CreateTextureDataFromPng("resources/loading.png", CONST_ZIP_FILE));
- loadingMesh.data = CreateRect2D({ Environment::width * 0.5, Environment::height * 0.5 }, { Environment::width * 0.5, Environment::height*0.5 }, 3);
+ loadingMesh.data = CreateRect2D({ Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, { Environment::projectionWidth * 0.5f, Environment::projectionHeight * 0.5f }, 3);
loadingMesh.RefreshVBO();
+#ifdef EMSCRIPTEN
+ // Asynchronously download resources.zip; setupPart2() is called on completion.
+ // The loading screen stays visible until the download finishes.
+ s_instance = this;
+ emscripten_async_wget("resources.zip", "resources.zip", onResourcesZipLoaded, onResourcesZipError);
+#else
mainThreadHandler.EnqueueMainThreadTask([this]() {
this->setupPart2();
- });
+ });
+#endif
}
@@ -103,15 +139,27 @@ namespace ZL
menuManager.setupMenu();
- menuManager.onSingleplayerPressed = [this]() {
- networkClient = std::make_unique();
+ menuManager.onSingleplayerPressed = [this](const std::string& nickname, int shipType) {
+ Environment::shipState.nickname = nickname;
+ Environment::shipState.shipType = shipType;
+
+ auto localClient = new LocalClient;
+ ClientState st = Environment::shipState;
+ st.id = localClient->GetClientId();
+ localClient->setLocalPlayerState(st);
+
+ networkClient = std::unique_ptr(localClient);
networkClient->Connect("", 0);
+
+ lastTickCount = 0;
spaceGameStarted = 1;
};
+ menuManager.onMultiplayerPressed = [this](const std::string& nickname, int shipType) {
+ Environment::shipState.nickname = nickname;
+ Environment::shipState.shipType = shipType;
+
- menuManager.onMultiplayerPressed = [this]() {
-#ifdef NETWORK
#ifdef EMSCRIPTEN
networkClient = std::make_unique();
networkClient->Connect("localhost", 8081);
@@ -119,7 +167,13 @@ namespace ZL
networkClient = std::make_unique(taskManager.getIOContext());
networkClient->Connect("localhost", 8081);
#endif
-#endif
+
+ if (networkClient) {
+ std::string joinMsg = std::string("JOIN:") + nickname + ":" + std::to_string(shipType);
+ networkClient->Send(joinMsg);
+ std::cerr << "Sent JOIN: " << joinMsg << std::endl;
+ }
+
lastTickCount = 0;
spaceGameStarted = 1;
};
@@ -195,8 +249,8 @@ namespace ZL
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
- float width = Environment::width;
- float height = Environment::height;
+ float width = Environment::projectionWidth;
+ float height = Environment::projectionHeight;
renderer.PushProjectionMatrix(
0, width,
@@ -281,14 +335,19 @@ namespace ZL
if (event.type == SDL_QUIT) {
Environment::exitGameLoop = true;
}
-#if SDL_VERSION_ATLEAST(2,0,5)
- else if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) {
+
+
+ if (event.type == SDL_WINDOWEVENT && event.window.event == SDL_WINDOWEVENT_RESIZED) {
// Обновляем размеры и сбрасываем кеш текстов, т.к. меши хранятся в пикселях
Environment::width = event.window.data1;
Environment::height = event.window.data2;
+ Environment::computeProjectionDimensions();
+ menuManager.uiManager.updateAllLayouts();
+ std::cout << "Window resized: " << Environment::width << "x" << Environment::height << std::endl;
+
space.clearTextRendererCache();
}
-#endif
+
#ifdef __ANDROID__
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_AC_BACK) {
Environment::exitGameLoop = true;
@@ -297,22 +356,38 @@ namespace ZL
#ifdef __ANDROID__
if (event.type == SDL_FINGERDOWN) {
- int mx = static_cast(event.tfinger.x * Environment::width);
- int my = static_cast(event.tfinger.y * Environment::height);
+ int mx = static_cast(event.tfinger.x * Environment::projectionWidth);
+ int my = static_cast(event.tfinger.y * Environment::projectionHeight);
handleDown(mx, my);
}
else if (event.type == SDL_FINGERUP) {
- int mx = static_cast(event.tfinger.x * Environment::width);
- int my = static_cast(event.tfinger.y * Environment::height);
+ int mx = static_cast(event.tfinger.x * Environment::projectionWidth);
+ int my = static_cast(event.tfinger.y * Environment::projectionHeight);
handleUp(mx, my);
}
else if (event.type == SDL_FINGERMOTION) {
- int mx = static_cast(event.tfinger.x * Environment::width);
- int my = static_cast(event.tfinger.y * Environment::height);
+ int mx = static_cast(event.tfinger.x * Environment::projectionWidth);
+ int my = static_cast(event.tfinger.y * Environment::projectionHeight);
handleMotion(mx, my);
}
#else
- if (event.type == SDL_MOUSEBUTTONDOWN) {
+
+
+ if (event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP) {
+ // Преобразуем экранные пиксели в проекционные единицы
+ int mx = static_cast((float)event.button.x / Environment::width * Environment::projectionWidth);
+ int my = static_cast((float)event.button.y / Environment::height * Environment::projectionHeight);
+
+ if (event.type == SDL_MOUSEBUTTONDOWN) handleDown(mx, my);
+ else handleUp(mx, my);
+ }
+ else if (event.type == SDL_MOUSEMOTION) {
+ int mx = static_cast((float)event.motion.x / Environment::width * Environment::projectionWidth);
+ int my = static_cast((float)event.motion.y / Environment::height * Environment::projectionHeight);
+ handleMotion(mx, my);
+ }
+
+ /*if (event.type == SDL_MOUSEBUTTONDOWN) {
int mx = event.button.x;
int my = event.button.y;
handleDown(mx, my);
@@ -326,7 +401,7 @@ namespace ZL
int mx = event.motion.x;
int my = event.motion.y;
handleMotion(mx, my);
- }
+ }*/
if (event.type == SDL_MOUSEWHEEL) {
static const float zoomstep = 2.0f;
@@ -383,7 +458,7 @@ namespace ZL
void Game::handleDown(int mx, int my)
{
int uiX = mx;
- int uiY = Environment::height - my;
+ int uiY = Environment::projectionHeight - my;
menuManager.uiManager.onMouseDown(uiX, uiY);
@@ -411,7 +486,7 @@ namespace ZL
void Game::handleUp(int mx, int my)
{
int uiX = mx;
- int uiY = Environment::height - my;
+ int uiY = Environment::projectionHeight - my;
menuManager.uiManager.onMouseUp(uiX, uiY);
@@ -426,7 +501,7 @@ namespace ZL
void Game::handleMotion(int mx, int my)
{
int uiX = mx;
- int uiY = Environment::height - my;
+ int uiY = Environment::projectionHeight - my;
menuManager.uiManager.onMouseMove(uiX, uiY);
diff --git a/src/Game.h b/src/Game.h
index 6e0da91..ab4ab9f 100644
--- a/src/Game.h
+++ b/src/Game.h
@@ -54,6 +54,12 @@ namespace ZL {
void handleUp(int mx, int my);
void handleMotion(int mx, int my);
+#ifdef EMSCRIPTEN
+ static Game* s_instance;
+ static void onResourcesZipLoaded(const char* filename);
+ static void onResourcesZipError(const char* filename);
+#endif
+
SDL_Window* window;
SDL_GLContext glContext;
diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp
index e14bd56..c5b072d 100644
--- a/src/MenuManager.cpp
+++ b/src/MenuManager.cpp
@@ -21,6 +21,7 @@ namespace ZL {
gameOverSavedRoot = loadUiFromFile("resources/config/game_over.json", renderer, CONST_ZIP_FILE);
+ auto shipSelectionRoot = loadUiFromFile("resources/config/ship_selection_menu.json", renderer, CONST_ZIP_FILE);
std::function loadGameplayUI;
loadGameplayUI = [this]() {
uiManager.replaceRoot(uiSavedRoot);
@@ -37,7 +38,7 @@ namespace ZL {
uiManager.startAnimationOnNode("backgroundNode", "bgScroll");
static bool isExitButtonAnimating = false;
uiManager.setAnimationCallback("settingsButton", "buttonsExit", [this]() {
- std::cerr << "Settings button animation finished -> переход в настройки" << std::endl;
+ std::cerr << "Settings button animation finished -> ??????? ? ?????????" << std::endl;
if (uiManager.pushMenuFromSavedRoot(settingsSavedRoot)) {
uiManager.setButtonCallback("Opt1", [this](const std::string& n) {
std::cerr << "Opt1 pressed: " << n << std::endl;
@@ -95,6 +96,7 @@ namespace ZL {
}
});
+
uiManager.setButtonCallback("shootButton", [this](const std::string& name) {
onFirePressed();
});
@@ -102,11 +104,12 @@ namespace ZL {
onFirePressed();
});
uiManager.setSliderCallback("velocitySlider", [this](const std::string& name, float value) {
+
int newVel = roundf(value * 10);
- /*if (newVel > 2)
+ if (newVel > 2)
{
newVel = 2;
- }*/
+ }
if (newVel != Environment::shipState.selectedVelocity) {
onVelocityChanged(newVel);
@@ -114,18 +117,75 @@ namespace ZL {
});
};
- uiManager.setButtonCallback("singleButton", [loadGameplayUI, this](const std::string& name) {
- std::cerr << "Single button pressed: " << name << " -> load gameplay UI\n";
- loadGameplayUI();
- onSingleplayerPressed();
- });
- uiManager.setButtonCallback("multiplayerButton", [loadGameplayUI, this](const std::string& name) {
- std::cerr << "Multiplayer button pressed: " << name << " -> load gameplay UI\n";
- loadGameplayUI();
- onMultiplayerPressed();
+ uiManager.setButtonCallback("singleButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
+ std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n";
+ if (!shipSelectionRoot) {
+ std::cerr << "Failed to load ship selection UI\n";
+ return;
+ }
+ if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) {
+ uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) {
+ std::string nick = uiManager.getTextFieldValue("nicknameInput");
+ if (nick.empty()) nick = "Player";
+ int shipType = 0;
+ uiManager.popMenu();
+ loadGameplayUI();
+ if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
+ });
+
+ uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) {
+ std::string nick = uiManager.getTextFieldValue("nicknameInput");
+ if (nick.empty()) nick = "Player";
+ int shipType = 1;
+ uiManager.popMenu();
+ loadGameplayUI();
+ if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
+ });
+
+ uiManager.setButtonCallback("backButton", [this](const std::string& btnName) {
+ uiManager.popMenu();
+ });
+ }
+ else {
+ std::cerr << "Failed to push ship selection menu\n";
+ }
});
- uiManager.setButtonCallback("multiplayerButton2", [this](const std::string& name) {
+ uiManager.setButtonCallback("multiplayerButton", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
+ std::cerr << "Multiplayer button pressed: " << name << " -> open ship selection UI\n";
+ if (!shipSelectionRoot) {
+ std::cerr << "Failed to load ship selection UI\n";
+ return;
+ }
+ if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) {
+ uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) {
+ std::string nick = uiManager.getTextFieldValue("nicknameInput");
+ if (nick.empty()) nick = "Player";
+ int shipType = 0;
+ uiManager.popMenu();
+ loadGameplayUI();
+ if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType);
+ });
+
+ uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) {
+ std::string nick = uiManager.getTextFieldValue("nicknameInput");
+ if (nick.empty()) nick = "Player";
+ int shipType = 1;
+ uiManager.popMenu();
+ loadGameplayUI();
+ if (onMultiplayerPressed) onMultiplayerPressed(nick, shipType);
+ });
+
+ uiManager.setButtonCallback("backButton", [this](const std::string& btnName) {
+ uiManager.popMenu();
+ });
+ }
+ else {
+ std::cerr << "Failed to push ship selection menu\n";
+ }
+ });
+
+ /*uiManager.setButtonCallback("multiplayerButton2", [this, shipSelectionRoot, loadGameplayUI](const std::string& name) {
std::cerr << "Multiplayer button pressed → opening multiplayer menu\n";
uiManager.startAnimationOnNode("playButton", "buttonsExit");
@@ -135,7 +195,6 @@ namespace ZL {
if (uiManager.pushMenuFromSavedRoot(multiplayerSavedRoot)) {
- // Callback для кнопки подключения
uiManager.setButtonCallback("connectButton", [this](const std::string& buttonName) {
std::string serverAddress = uiManager.getTextFieldValue("serverInputField");
@@ -147,16 +206,12 @@ namespace ZL {
uiManager.setText("statusText", "Connecting to " + serverAddress + "...");
std::cerr << "Connecting to server: " << serverAddress << std::endl;
- // Здесь добавить вашу логику подключения к серверу
- // connectToServer(serverAddress);
});
- // Callback для кнопки назад
uiManager.setButtonCallback("backButton", [this](const std::string& buttonName) {
uiManager.popMenu();
});
- // Callback для отслеживания ввода текста
uiManager.setTextFieldCallback("serverInputField",
[this](const std::string& fieldName, const std::string& newText) {
std::cout << "Server input field changed to: " << newText << std::endl;
@@ -167,21 +222,55 @@ namespace ZL {
else {
std::cerr << "Failed to load multiplayer menu\n";
}
+ std::cerr << "Single button pressed: " << name << " -> open ship selection UI\n";
+ if (!shipSelectionRoot) {
+ std::cerr << "Failed to load ship selection UI\n";
+ return;
+ }
+ if (uiManager.pushMenuFromSavedRoot(shipSelectionRoot)) {
+ uiManager.setButtonCallback("spaceshipButton", [this, loadGameplayUI](const std::string& btnName) {
+ std::string nick = uiManager.getTextFieldValue("nicknameInput");
+ if (nick.empty()) nick = "Player";
+ int shipType = 0;
+ uiManager.popMenu();
+ loadGameplayUI();
+ if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
+ });
+
+ uiManager.setButtonCallback("cargoshipButton", [this, loadGameplayUI](const std::string& btnName) {
+ std::string nick = uiManager.getTextFieldValue("nicknameInput");
+ if (nick.empty()) nick = "Player";
+ int shipType = 1;
+ uiManager.popMenu();
+ loadGameplayUI();
+ if (onSingleplayerPressed) onSingleplayerPressed(nick, shipType);
+ });
+
+ uiManager.setButtonCallback("backButton", [this](const std::string& btnName) {
+ uiManager.popMenu();
+ });
+ }
+ else {
+ std::cerr << "Failed to push ship selection menu\n";
+ }
});
uiManager.setButtonCallback("exitButton", [](const std::string& name) {
std::cerr << "Exit from main menu pressed: " << name << " -> exiting\n";
Environment::exitGameLoop = true;
- });
+ });*/
}
- void MenuManager::showGameOver()
+ void MenuManager::showGameOver(int score)
{
if (!uiGameOverShown) {
if (uiManager.pushMenuFromSavedRoot(gameOverSavedRoot)) {
+ uiManager.setText("scoreText", std::string("Score: ") + std::to_string(score));
+
uiManager.setButtonCallback("restartButton", [this](const std::string& name) {
+ uiManager.setText("scoreText", "");
uiGameOverShown = false;
uiManager.popMenu();
- onRestartPressed();
+ if (onRestartPressed) onRestartPressed();
});
uiManager.setButtonCallback("gameOverExitButton", [this](const std::string& name) {
diff --git a/src/MenuManager.h b/src/MenuManager.h
index 7ad6d87..0b33f2b 100644
--- a/src/MenuManager.h
+++ b/src/MenuManager.h
@@ -28,14 +28,15 @@ namespace ZL {
void setupMenu();
- void showGameOver();
+ //void showGameOver();
+ void showGameOver(int score);
std::function onRestartPressed;
std::function onVelocityChanged;
std::function onFirePressed;
- std::function onSingleplayerPressed;
- std::function onMultiplayerPressed;
+ std::function onSingleplayerPressed;
+ std::function onMultiplayerPressed;
};
};
diff --git a/src/Projectile.cpp b/src/Projectile.cpp
index 419419d..9f98d4f 100644
--- a/src/Projectile.cpp
+++ b/src/Projectile.cpp
@@ -41,7 +41,7 @@ namespace ZL {
}
void Projectile::rebuildMesh(Renderer&) {
- float half = size * 0.5f;
+ float half = 10 * size * 0.5f;
mesh.data.PositionData.clear();
mesh.data.TexCoordData.clear();
diff --git a/src/Projectile.h b/src/Projectile.h
index 872177f..e72552e 100644
--- a/src/Projectile.h
+++ b/src/Projectile.h
@@ -3,6 +3,7 @@
#include "render/Renderer.h"
#include "render/TextureManager.h"
#include
+#include "SparkEmitter.h"
namespace ZL {
@@ -19,6 +20,8 @@ namespace ZL {
Vector3f getPosition() const { return pos; }
void deactivate() { active = false; }
+
+ SparkEmitter projectileEmitter;
private:
Vector3f pos;
Vector3f vel;
diff --git a/src/Space.cpp b/src/Space.cpp
index f69e7d0..3639912 100644
--- a/src/Space.cpp
+++ b/src/Space.cpp
@@ -159,15 +159,15 @@ namespace ZL
// В пределах экрана?
// (можно оставить, можно клампить)
- float sx = (ndc.x() * 0.5f + 0.5f) * Environment::width;
- float sy = (ndc.y() * 0.5f + 0.5f) * Environment::height;
+ float sx = (ndc.x() * 0.5f + 0.5f) * Environment::projectionWidth;
+ float sy = (ndc.y() * 0.5f + 0.5f) * Environment::projectionHeight;
outX = sx;
outY = sy;
// Можно отсеять те, что вне:
- if (sx < -200 || sx > Environment::width + 200) return false;
- if (sy < -200 || sy > Environment::height + 200) return false;
+ if (sx < -200 || sx > Environment::projectionWidth + 200) return false;
+ if (sy < -200 || sy > Environment::projectionHeight + 200) return false;
return true;
}
@@ -267,6 +267,16 @@ namespace ZL
Environment::zoom = DEFAULT_ZOOM;
Environment::tapDownHold = false;
+ if (networkClient) {
+ try {
+ networkClient->Send(std::string("RESPAWN"));
+ std::cout << "Client: Sent RESPAWN to server\n";
+ }
+ catch (...) {
+ std::cerr << "Client: Failed to send RESPAWN\n";
+ }
+ }
+ this->playerScore = 0;
std::cerr << "Game restarted\n";
};
@@ -286,12 +296,12 @@ namespace ZL
cubemapTexture = std::make_shared(
std::array{
- CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
- CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
- CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
- CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
- CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE),
- CreateTextureDataFromPng("resources/sky/space_red.png", CONST_ZIP_FILE)
+ CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
+ CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
+ CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
+ CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
+ CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE),
+ CreateTextureDataFromPng("resources/sky/space1.png", CONST_ZIP_FILE)
});
@@ -310,6 +320,22 @@ namespace ZL
spaceship.AssignFrom(spaceshipBase);
spaceship.RefreshVBO();
+ // Load cargo
+ cargoTexture = std::make_shared(CreateTextureDataFromPng("resources/Cargo_Base_color_sRGB.png", CONST_ZIP_FILE));
+ cargoBase = LoadFromTextFile02("resources/cargoship001.txt", CONST_ZIP_FILE);
+ auto quat = Eigen::Quaternionf(Eigen::AngleAxisf(-M_PI * 0.5, Eigen::Vector3f::UnitZ()));
+ auto rotMatrix = quat.toRotationMatrix();
+ cargoBase.RotateByMatrix(rotMatrix);
+
+ auto quat2 = Eigen::Quaternionf(Eigen::AngleAxisf(M_PI * 0.5, Eigen::Vector3f::UnitY()));
+ auto rotMatrix2 = quat2.toRotationMatrix();
+ cargoBase.RotateByMatrix(rotMatrix2);
+ //cargoBase.RotateByMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(M_PI, Eigen::Vector3f::UnitY())).toRotationMatrix());
+ cargoBase.Move(Vector3f{ 0, 0, -5 });
+ cargo.AssignFrom(cargoBase);
+ cargo.RefreshVBO();
+
+ //projectileTexture = std::make_shared(CreateTextureDataFromPng("resources/spark2.png", CONST_ZIP_FILE));
//Boxes
boxTexture = std::make_unique(CreateTextureDataFromPng("resources/box/box.png", CONST_ZIP_FILE));
@@ -336,6 +362,19 @@ namespace ZL
throw std::runtime_error("Failed to load spark emitter config file!");
}
+ crosshairCfgLoaded = loadCrosshairConfig("resources/config/crosshair_config.json");
+ std::cout << "[Crosshair] loaded=" << crosshairCfgLoaded
+ << " enabled=" << crosshairCfg.enabled
+ << " w=" << Environment::width << " h=" << Environment::height
+ << " alpha=" << crosshairCfg.alpha
+ << " thickness=" << crosshairCfg.thicknessPx
+ << " gap=" << crosshairCfg.gapPx << "\n";
+ if (!crosshairCfgLoaded) {
+ std::cerr << "Failed to load crosshair_config.json, using defaults\n";
+ }
+
+
+
textRenderer = std::make_unique();
if (!textRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 32, CONST_ZIP_FILE)) {
std::cerr << "Failed to init TextRenderer\n";
@@ -439,20 +478,38 @@ namespace ZL
renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset
if (shipAlive) {
- glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
- renderer.DrawVertexRenderStruct(spaceship);
+ if (Environment::shipState.shipType == 1 && cargoTexture) {
+ glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID());
+ renderer.DrawVertexRenderStruct(cargo);
+ }
+ else {
+ glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
+ renderer.DrawVertexRenderStruct(spaceship);
+ }
}
renderer.PopMatrix();
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ renderer.shaderManager.PushShader("default");
+ renderer.RenderUniform1i(textureUniformName, 0);
+ renderer.EnableVertexAttribArray(vPositionName);
+ renderer.EnableVertexAttribArray(vTexCoordName);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE);
for (const auto& p : projectiles) {
if (p && p->isActive()) {
p->draw(renderer);
+ p->projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
}
}
+ glDisable(GL_BLEND);
- projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
+ renderer.DisableVertexAttribArray(vPositionName);
+ renderer.DisableVertexAttribArray(vTexCoordName);
+ renderer.shaderManager.PopShader();
+
+ //projectileEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
if (shipAlive) {
renderer.PushMatrix();
@@ -566,18 +623,17 @@ namespace ZL
drawBoxesLabels();
drawShip();
+ drawCrosshair();
drawTargetHud();
CheckGlError();
}
void Space::drawRemoteShips() {
- // Используем те же константы имен для шейдеров, что и в drawShip
static const std::string defaultShaderName = "default";
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
- // Активируем шейдер и текстуру (предполагаем, что меш у всех одинаковый)
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
@@ -588,19 +644,17 @@ namespace ZL
static_cast(Environment::width) / static_cast(Environment::height),
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
- // Биндим текстуру корабля один раз для всех удаленных игроков (оптимизация батчинга)
- glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
-
- // Если сервер прислал коробки, применяем их однократно вместо локальной генерации
if (!serverBoxesApplied && networkClient) {
auto sboxes = networkClient->getServerBoxes();
+ auto destroyedFlags = networkClient->getServerBoxDestroyedFlags();
if (!sboxes.empty()) {
boxCoordsArr.clear();
- for (auto& b : sboxes) {
+ boxCoordsArr.resize(sboxes.size());
+ for (size_t i = 0; i < sboxes.size(); ++i) {
BoxCoords bc;
- bc.pos = b.first;
- bc.m = b.second;
- boxCoordsArr.push_back(bc);
+ bc.pos = sboxes[i].first;
+ bc.m = sboxes[i].second;
+ boxCoordsArr[i] = bc;
}
boxRenderArr.resize(boxCoordsArr.size());
for (int i = 0; i < (int)boxCoordsArr.size(); ++i) {
@@ -608,11 +662,15 @@ namespace ZL
boxRenderArr[i].RefreshVBO();
}
boxAlive.assign(boxCoordsArr.size(), true);
+ if (destroyedFlags.size() == boxAlive.size()) {
+ for (size_t i = 0; i < boxAlive.size(); ++i) {
+ if (destroyedFlags[i]) boxAlive[i] = false;
+ }
+ }
serverBoxesApplied = true;
}
}
- // Итерируемся по актуальным данным из extrapolateRemotePlayers
for (auto const& [id, remotePlayer] : remotePlayerStates) {
const ClientState& playerState = remotePlayer;
@@ -622,7 +680,7 @@ namespace ZL
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0,0, -1.0f * Environment::zoom });
- renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset
+ //renderer.TranslateMatrix({ 0, -6.f, 0 }); //Ship camera offset
renderer.RotateMatrix(Environment::inverseShipMatrix);
renderer.TranslateMatrix(-Environment::shipState.position);
@@ -630,10 +688,16 @@ namespace ZL
Eigen::Vector3f relativePos = playerState.position;// -Environment::shipPosition;
renderer.TranslateMatrix(relativePos);
- // 3. Поворот врага
renderer.RotateMatrix(playerState.rotation);
- renderer.DrawVertexRenderStruct(spaceship);
+ if (playerState.shipType == 1 && cargoTexture) {
+ glBindTexture(GL_TEXTURE_2D, cargoTexture->getTexID());
+ renderer.DrawVertexRenderStruct(cargo);
+ }
+ else {
+ glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
+ renderer.DrawVertexRenderStruct(spaceship);
+ }
renderer.PopMatrix();
}
@@ -649,8 +713,6 @@ namespace ZL
{
if (!textRenderer) return;
- //#ifdef NETWORK
- // 2D поверх 3D
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@@ -660,18 +722,12 @@ namespace ZL
if (deadRemotePlayers.count(id)) continue;
const ClientState& st = remotePlayer;
- // Позиция корабля в мире
Vector3f shipWorld = st.position;
float distSq = (Environment::shipState.position - shipWorld).squaredNorm();
- /*if (distSq > MAX_DIST_SQ) // дальность прорисовки никнейма
- continue;*/
float dist = sqrt(distSq);
- float alpha = 1.0f; // постоянная видимость
- /*float alpha = std::clamp(1.f - (dist - FADE_START) / FADE_RANGE, 0.f, 1.f); // дальность прорисовки никнейма
- if (alpha < 0.01f)
- continue; */
- Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f }; // регулировка высоты
+ float alpha = 1.0f;
+ Vector3f labelWorld = shipWorld + Vector3f{ 0.f, -4.f, 0.f };
float sx, sy, depth;
if (!worldToScreen(labelWorld, sx, sy, depth))
continue;
@@ -679,30 +735,259 @@ namespace ZL
float uiX = sx, uiY = sy;
float scale = std::clamp(BASE_SCALE / (dist * PERSPECTIVE_K + 1.f), MIN_SCALE, MAX_SCALE);
- // Дефолтный лейбл
- std::string label = "Player (" + std::to_string(st.id) + ") " + std::to_string((int)dist) + "m";
+ std::string displayName;
+ if (!st.nickname.empty() && st.nickname != "Player") {
+ displayName = st.nickname;
+ }
+ else {
+ displayName = "Player (" + std::to_string(st.id) + ")";
+ }
+ std::string label = displayName + " " + std::to_string((int)dist) + "m";
- // TODO: nickname sync
-
- textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha }); // color param
+ textRenderer->drawText(label, uiX + 1.f, uiY + 1.f, scale, true, { 0.f, 0.f, 0.f, alpha });
textRenderer->drawText(label, uiX, uiY, scale, true, { 1.f, 1.f, 1.f, alpha });
}
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
- //#endif
+ }
+
+ // хелпер прицела: добавляет повернутую 2D-линию в меш прицела
+ static void AppendRotatedRect2D(
+ VertexDataStruct& out,
+ float cx, float cy,
+ float length, float thickness,
+ float angleRad,
+ float z,
+ const Eigen::Vector3f& rgb)
+ {
+ // прямоугольник вдоль локальной оси +X: [-L/2..+L/2] и [-T/2..+T/2]
+ float hl = length * 0.5f;
+ float ht = thickness * 0.5f;
+
+ Eigen::Vector2f p0(-hl, -ht);
+ Eigen::Vector2f p1(-hl, +ht);
+ Eigen::Vector2f p2(+hl, +ht);
+ Eigen::Vector2f p3(+hl, -ht);
+
+ float c = std::cos(angleRad);
+ float s = std::sin(angleRad);
+
+ auto rot = [&](const Eigen::Vector2f& p) -> Vector3f {
+ float rx = p.x() * c - p.y() * s;
+ float ry = p.x() * s + p.y() * c;
+ return Vector3f(cx + rx, cy + ry, z);
+ };
+
+ Vector3f v0 = rot(p0);
+ Vector3f v1 = rot(p1);
+ Vector3f v2 = rot(p2);
+ Vector3f v3 = rot(p3);
+
+ // 2 треугольника
+ out.PositionData.push_back(v0);
+ out.PositionData.push_back(v1);
+ out.PositionData.push_back(v2);
+ out.PositionData.push_back(v2);
+ out.PositionData.push_back(v3);
+ out.PositionData.push_back(v0);
+
+ for (int i = 0; i < 6; ++i) out.ColorData.push_back(rgb);
+ }
+
+ bool Space::loadCrosshairConfig(const std::string& path)
+ {
+ using json = nlohmann::json;
+
+ std::string content;
+ try {
+ if (std::string(CONST_ZIP_FILE).empty()) content = readTextFile(path);
+ else {
+ auto buf = readFileFromZIP(path, CONST_ZIP_FILE);
+ if (buf.empty()) return false;
+ content.assign(buf.begin(), buf.end());
+ }
+ json j = json::parse(content);
+
+ if (j.contains("enabled")) crosshairCfg.enabled = j["enabled"].get();
+
+ if (j.contains("referenceResolution") && j["referenceResolution"].is_array() && j["referenceResolution"].size() == 2) {
+ crosshairCfg.refW = j["referenceResolution"][0].get();
+ crosshairCfg.refH = j["referenceResolution"][1].get();
+ }
+
+ if (j.contains("scale")) crosshairCfg.scaleMul = j["scale"].get();
+ crosshairCfg.scaleMul = std::clamp(crosshairCfg.scaleMul, 0.1f, 3.0f);
+
+ if (j.contains("color") && j["color"].is_array() && j["color"].size() == 3) {
+ crosshairCfg.color = Eigen::Vector3f(
+ j["color"][0].get(),
+ j["color"][1].get(),
+ j["color"][2].get()
+ );
+ }
+
+ if (j.contains("cl_crosshairalpha")) crosshairCfg.alpha = j["cl_crosshairalpha"].get();
+ if (j.contains("cl_crosshairthickness")) crosshairCfg.thicknessPx = j["cl_crosshairthickness"].get();
+ if (j.contains("centerGapPx")) crosshairCfg.gapPx = j["centerGapPx"].get();
+
+ if (j.contains("top") && j["top"].is_object()) {
+ auto t = j["top"];
+ if (t.contains("lengthPx")) crosshairCfg.topLenPx = t["lengthPx"].get();
+ if (t.contains("angleDeg")) crosshairCfg.topAngleDeg = t["angleDeg"].get();
+ }
+
+ crosshairCfg.arms.clear();
+ if (j.contains("arms") && j["arms"].is_array()) {
+ for (auto& a : j["arms"]) {
+ CrosshairConfig::Arm arm;
+ arm.lenPx = a.value("lengthPx", 20.0f);
+ arm.angleDeg = a.value("angleDeg", 210.0f);
+ crosshairCfg.arms.push_back(arm);
+ }
+ }
+ else {
+ // дефолт
+ crosshairCfg.arms.push_back({ 20.0f, 210.0f });
+ crosshairCfg.arms.push_back({ 20.0f, 330.0f });
+ }
+
+ // clamp
+ crosshairCfg.alpha = std::clamp(crosshairCfg.alpha, 0.0f, 1.0f);
+ crosshairCfg.thicknessPx = max(0.5f, crosshairCfg.thicknessPx);
+ crosshairCfg.gapPx = max(0.0f, crosshairCfg.gapPx);
+
+ crosshairMeshValid = false; // пересобрать
+ return true;
+ }
+ catch (...) {
+ return false;
+ }
+ }
+
+ // пересобирает mesh прицела при изменениях/ресайзе
+ void Space::rebuildCrosshairMeshIfNeeded()
+ {
+ if (!crosshairCfg.enabled) return;
+
+ // если ничего не изменилось — не трогаем VBO
+ if (crosshairMeshValid &&
+ crosshairLastW == Environment::projectionWidth &&
+ crosshairLastH == Environment::projectionHeight &&
+ std::abs(crosshairLastAlpha - crosshairCfg.alpha) < 1e-6f &&
+ std::abs(crosshairLastThickness - crosshairCfg.thicknessPx) < 1e-6f &&
+ std::abs(crosshairLastGap - crosshairCfg.gapPx) < 1e-6f &&
+ std::abs(crosshairLastScaleMul - crosshairCfg.scaleMul) < 1e-6f)
+ {
+ return;
+ }
+
+ crosshairLastW = Environment::projectionWidth;
+ crosshairLastH = Environment::projectionHeight;
+ crosshairLastAlpha = crosshairCfg.alpha;
+ crosshairLastThickness = crosshairCfg.thicknessPx;
+ crosshairLastGap = crosshairCfg.gapPx;
+ crosshairLastScaleMul = crosshairCfg.scaleMul;
+
+ float cx = Environment::projectionWidth * 0.5f;
+ float cy = Environment::projectionHeight * 0.5f;
+
+ // масштаб от reference (стандартно: по высоте)
+ float scale = (crosshairCfg.refH > 0) ? (Environment::projectionHeight / (float)crosshairCfg.refH) : 1.0f;
+ scale *= crosshairCfg.scaleMul;
+
+ float thickness = crosshairCfg.thicknessPx * scale;
+ float gap = crosshairCfg.gapPx * scale;
+
+ VertexDataStruct v;
+ v.PositionData.reserve(6 * (1 + (int)crosshairCfg.arms.size()));
+ v.ColorData.reserve(6 * (1 + (int)crosshairCfg.arms.size()));
+
+ const float z = 0.0f;
+ const Eigen::Vector3f rgb = crosshairCfg.color;
+
+ auto deg2rad = [](float d) { return d * 3.1415926535f / 180.0f; };
+
+ // TOP (короткая палочка сверху)
+ {
+ float len = crosshairCfg.topLenPx * scale;
+ float ang = deg2rad(crosshairCfg.topAngleDeg);
+
+ // сдвигаем сегмент от центра на gap + len/2 по направлению
+ float dx = std::cos(ang);
+ float dy = std::sin(ang);
+ float mx = cx + dx * (gap + len * 0.5f);
+ float my = cy + dy * (gap + len * 0.5f);
+
+ AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb);
+ }
+
+ // ARMS (2 луча вниз-влево и вниз-вправо)
+ for (auto& a : crosshairCfg.arms)
+ {
+ float len = a.lenPx * scale;
+ float ang = deg2rad(a.angleDeg);
+
+ float dx = std::cos(ang);
+ float dy = std::sin(ang);
+ float mx = cx + dx * (gap + len * 0.5f);
+ float my = cy + dy * (gap + len * 0.5f);
+
+ AppendRotatedRect2D(v, mx, my, len, thickness, ang, z, rgb);
+ }
+
+ crosshairMesh.AssignFrom(v);
+ crosshairMesh.RefreshVBO();
+ crosshairMeshValid = true;
+ }
+
+ void Space::drawCrosshair()
+ {
+ if (!crosshairCfg.enabled) return;
+
+ rebuildCrosshairMeshIfNeeded();
+ if (!crosshairMeshValid) return;
+
+ glDisable(GL_DEPTH_TEST);
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ renderer.shaderManager.PushShader("defaultColor");
+ renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
+ renderer.PushMatrix();
+ renderer.LoadIdentity();
+
+ renderer.EnableVertexAttribArray("vPosition");
+ renderer.EnableVertexAttribArray("vColor");
+
+ Eigen::Vector4f uColor(crosshairCfg.color.x(), crosshairCfg.color.y(), crosshairCfg.color.z(), crosshairCfg.alpha);
+ renderer.RenderUniform4fv("uColor", uColor.data());
+
+ renderer.DrawVertexRenderStruct(crosshairMesh);
+
+ renderer.DisableVertexAttribArray("vPosition");
+ renderer.DisableVertexAttribArray("vColor");
+
+ renderer.PopMatrix();
+ renderer.PopProjectionMatrix();
+ renderer.shaderManager.PopShader();
+
+ glDisable(GL_BLEND);
+ glEnable(GL_DEPTH_TEST);
}
int Space::pickTargetId() const
{
int bestId = -1;
- constexpr float INF_F = 1e30f;
- float bestDistSq = INF_F;
+ float bestDistSq = 1e30f;
for (auto const& [id, st] : remotePlayerStates) {
if (deadRemotePlayers.count(id)) continue;
float d2 = (Environment::shipState.position - st.position).squaredNorm();
+
+ if (d2 > TARGET_MAX_DIST_SQ) continue; // слишком далеко
+
if (d2 < bestDistSq) {
bestDistSq = d2;
bestId = id;
@@ -711,6 +996,95 @@ namespace ZL
return bestId;
}
+ static Vector3f ForwardFromRotation(const Matrix3f& rot)
+ {
+ Vector3f localForward(0, 0, -1);
+ Vector3f worldForward = rot * localForward;
+ float len = worldForward.norm();
+ if (len > 1e-6f) worldForward /= len;
+ return worldForward;
+ }
+
+ static bool SolveLeadInterceptTime(
+ const Vector3f& shooterPos,
+ const Vector3f& shooterVel,
+ const Vector3f& targetPos,
+ const Vector3f& targetVel,
+ float projectileSpeed, // muzzle speed (например 60)
+ float& outT)
+ {
+ Vector3f r = targetPos - shooterPos;
+ Vector3f v = targetVel - shooterVel;
+ float S = projectileSpeed;
+
+ float a = v.dot(v) - S * S;
+ float b = 2.0f * r.dot(v);
+ float c = r.dot(r);
+
+ // Если a почти 0 -> линейный случай
+ if (std::abs(a) < 1e-6f) {
+ if (std::abs(b) < 1e-6f) return false; // нет решения
+ float t = -c / b;
+ if (t > 0.0f) { outT = t; return true; }
+ return false;
+ }
+
+ float disc = b * b - 4.0f * a * c;
+ if (disc < 0.0f) return false;
+
+ float sqrtDisc = std::sqrt(disc);
+ float t1 = (-b - sqrtDisc) / (2.0f * a);
+ float t2 = (-b + sqrtDisc) / (2.0f * a);
+
+ float t = 1e30f;
+ if (t1 > 0.0f) t = min(t, t1);
+ if (t2 > 0.0f) t = min(t, t2);
+
+ if (t >= 1e29f) return false;
+ outT = t;
+ return true;
+ }
+
+ static VertexDataStruct MakeRing2D(
+ float cx, float cy,
+ float innerR, float outerR,
+ float z,
+ int segments,
+ const Eigen::Vector4f& rgba)
+ {
+ VertexDataStruct v;
+ v.PositionData.reserve(segments * 6);
+ v.ColorData.reserve(segments * 6);
+
+ Vector3f rgb(rgba.x(), rgba.y(), rgba.z());
+
+ const float twoPi = 6.28318530718f;
+ for (int i = 0; i < segments; ++i) {
+ float a0 = twoPi * (float)i / (float)segments;
+ float a1 = twoPi * (float)(i + 1) / (float)segments;
+
+ float c0 = std::cos(a0), s0 = std::sin(a0);
+ float c1 = std::cos(a1), s1 = std::sin(a1);
+
+ Vector3f p0i(cx + innerR * c0, cy + innerR * s0, z);
+ Vector3f p0o(cx + outerR * c0, cy + outerR * s0, z);
+ Vector3f p1i(cx + innerR * c1, cy + innerR * s1, z);
+ Vector3f p1o(cx + outerR * c1, cy + outerR * s1, z);
+
+ // два треугольника (p0i,p0o,p1o) и (p0i,p1o,p1i)
+ v.PositionData.push_back(p0i);
+ v.PositionData.push_back(p0o);
+ v.PositionData.push_back(p1o);
+
+ v.PositionData.push_back(p0i);
+ v.PositionData.push_back(p1o);
+ v.PositionData.push_back(p1i);
+
+ for (int k = 0; k < 6; ++k) v.ColorData.push_back(rgb);
+ }
+ return v;
+ }
+
static VertexDataStruct MakeColoredRect2D(float cx, float cy, float hw, float hh, float z,
const Eigen::Vector4f& rgba)
{
@@ -725,11 +1099,16 @@ namespace ZL
// defaultColor shader likely uses vColor (vec3), но нам нужен alpha.
// У тебя в Renderer есть RenderUniform4fv, но шейдер может брать vColor.
- // Поэтому: сделаем ColorData vec3, а alpha дадим через uniform uColor, если есть.
- // Если в defaultColor нет uniform uColor — тогда alpha будет 1.0.
- // Для совместимости: кладём RGB, alpha будем задавать uniform'ом отдельно.
+ // Поэтому: сделаем ColorData vec3, а alpha будем задавать uniform'ом отдельно.
Vector3f rgb{ rgba.x(), rgba.y(), rgba.z() };
v.ColorData = { rgb, rgb, rgb, rgb, rgb, rgb };
+
+ // defaultColor vertex shader expects vNormal and vTexCoord; provide valid values
+ // so WebGL/GLSL doesn't get NaN from normalize(vec3(0,0,0)).
+ const Vector3f n{ 0.f, 0.f, 1.f };
+ v.NormalData = { n, n, n, n, n, n };
+ const Vector2f uv{ 0.f, 0.f };
+ v.TexCoordData = { uv, uv, uv, uv, uv, uv };
return v;
}
@@ -756,6 +1135,51 @@ namespace ZL
const ClientState& st = remotePlayerStates.at(trackedTargetId);
Vector3f shipWorld = st.position;
+ // Lead Indicator
+ // скорость пули (как в fireProjectiles)
+ const float projectileSpeed = PROJECTILE_VELOCITY;
+
+ // позиция вылета
+ Vector3f shooterPos = Environment::shipState.position + Environment::shipState.rotation * Vector3f{ 0.0f, 0.9f - 6.0f, 5.0f };
+
+ // скорость цели в мире (вектор)
+ Vector3f shooterVel = ForwardFromRotation(Environment::shipState.rotation) * Environment::shipState.velocity;
+ Vector3f targetVel = ForwardFromRotation(st.rotation) * st.velocity;
+
+ const float minTargetSpeed = 0.5f; // подобрать (в твоих единицах)
+ bool targetMoving = (targetVel.norm() > minTargetSpeed);
+
+ // альфа круга
+ float leadAlpha = targetMoving ? 1.0f : 0.5f;
+
+ Vector3f leadWorld = shipWorld;
+ bool haveLead = false;
+
+ // чтобы круг не улетал далеко: максимум 4 секунды (подстроить под игру)
+ float distToTarget = (Environment::shipState.position - shipWorld).norm();
+ float maxLeadTime = std::clamp((distToTarget / projectileSpeed) * 1.2f, 0.05f, 4.0f);
+
+ if (!targetMoving) {
+ // Цель стоит: рисуем lead прямо на ней, но полупрозрачный
+ leadWorld = shipWorld;
+ haveLead = true;
+ }
+ else {
+ float tLead = 0.0f;
+
+ // 1) Пытаемся “правильное” решение перехвата
+ bool ok = SolveLeadInterceptTime(shooterPos, shooterVel, shipWorld, targetVel, projectileSpeed, tLead);
+
+ // 2) Если решения нет / оно плохое — fallback (чтобы круг не пропадал при пролёте "вбок")
+ // Это ключевое изменение: lead всегда будет.
+ if (!ok || !(tLead > 0.0f) || tLead > maxLeadTime) {
+ tLead = std::clamp(distToTarget / projectileSpeed, 0.05f, maxLeadTime);
+ }
+
+ leadWorld = shipWorld + targetVel * tLead;
+ haveLead = true;
+ }
+
// 2) проекция
float ndcX, ndcY, ndcZ, clipW;
if (!projectToNDC(shipWorld, ndcX, ndcY, ndcZ, clipW)) return;
@@ -774,7 +1198,7 @@ namespace ZL
// time for arrow bob
float t = static_cast(SDL_GetTicks64()) * 0.001f;
- // 4) Настройки стиля (как X3)
+ // 4) Настройки стиля
Eigen::Vector4f enemyColor(1.f, 0.f, 0.f, 1.f); // красный
float thickness = 2.0f; // толщина линий (px)
float z = 0.0f; // 2D слой
@@ -783,8 +1207,8 @@ namespace ZL
if (onScreen)
{
// перевод NDC -> экран (в пикселях)
- float sx = (ndcX * 0.5f + 0.5f) * Environment::width;
- float sy = (ndcY * 0.5f + 0.5f) * Environment::height;
+ float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
+ float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
// анимация “снаружи внутрь”
// targetAcquireAnim растёт к 1, быстро (похоже на захват)
@@ -820,28 +1244,63 @@ namespace ZL
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glClear(GL_DEPTH_BUFFER_BIT);
renderer.shaderManager.PushShader("defaultColor");
- renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f);
+ renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
renderer.PushMatrix();
renderer.LoadIdentity();
- // верх-лево: горизонт + вертикаль
+ renderer.EnableVertexAttribArray("vPosition");
+
+ Eigen::Vector4f hudColor = enemyColor;
+ renderer.RenderUniform4fv("uColor", hudColor.data());
+
+
+ if (haveLead) {
+ float leadNdcX, leadNdcY, leadNdcZ, leadClipW;
+ if (projectToNDC(leadWorld, leadNdcX, leadNdcY, leadNdcZ, leadClipW) && leadClipW > 0.0f) {
+ if (leadNdcX >= -1 && leadNdcX <= 1 && leadNdcY >= -1 && leadNdcY <= 1) {
+ float lx = (leadNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
+ float ly = (leadNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
+
+ float distLead = (Environment::shipState.position - leadWorld).norm();
+ float r = 30.0f / (distLead * 0.01f + 1.0f);
+ r = std::clamp(r, 6.0f, 18.0f);
+
+ float thicknessPx = 2.5f;
+ float innerR = max(1.0f, r - thicknessPx);
+ float outerR = r + thicknessPx;
+ Eigen::Vector4f leadColor = enemyColor;
+ leadColor.w() = leadAlpha;
+ renderer.RenderUniform4fv("uColor", leadColor.data());
+
+ VertexDataStruct ring = MakeRing2D(lx, ly, innerR, outerR, 0.0f, 32, enemyColor);
+ hudTempMesh.AssignFrom(ring);
+ renderer.DrawVertexRenderStruct(hudTempMesh);
+
+ renderer.RenderUniform4fv("uColor", hudColor.data());
+ }
+ }
+ }
+
+ renderer.EnableVertexAttribArray("vPosition");
+
drawBar(left + cornerLen * 0.5f, top, cornerLen, thickness);
drawBar(left, top - cornerLen * 0.5f, thickness, cornerLen);
- // верх-право
drawBar(right - cornerLen * 0.5f, top, cornerLen, thickness);
drawBar(right, top - cornerLen * 0.5f, thickness, cornerLen);
- // низ-лево
drawBar(left + cornerLen * 0.5f, bottom, cornerLen, thickness);
drawBar(left, bottom + cornerLen * 0.5f, thickness, cornerLen);
- // низ-право
drawBar(right - cornerLen * 0.5f, bottom, cornerLen, thickness);
drawBar(right, bottom + cornerLen * 0.5f, thickness, cornerLen);
+ renderer.DisableVertexAttribArray("vPosition");
+
+
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
@@ -853,12 +1312,9 @@ namespace ZL
return;
}
- // 6) Если цель offscreen: рисуем стрелку на краю
- // dir: куда “смотреть” в NDC
float dirX = ndcX;
float dirY = ndcY;
- // если позади камеры — разворачиваем направление
if (behind) {
dirX = -dirX;
dirY = -dirY;
@@ -869,7 +1325,6 @@ namespace ZL
dirX /= len;
dirY /= len;
- // пересечение луча с прямоугольником [-1..1] с отступом
float marginNdc = 0.08f;
float maxX = 1.0f - marginNdc;
float maxY = 1.0f - marginNdc;
@@ -881,19 +1336,16 @@ namespace ZL
float edgeNdcX = dirX * k;
float edgeNdcY = dirY * k;
- float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::width;
- float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::height;
+ float edgeX = (edgeNdcX * 0.5f + 0.5f) * Environment::projectionWidth;
+ float edgeY = (edgeNdcY * 0.5f + 0.5f) * Environment::projectionHeight;
- // лёгкая анимация “зова”: смещение по направлению
float bob = std::sin(t * 6.0f) * 6.0f;
edgeX += dirX * bob;
edgeY += dirY * bob;
- // стрелка как треугольник + маленький “хвост”
float arrowLen = 26.0f;
float arrowWid = 14.0f;
- // перпендикуляр
float px = -dirY;
float py = dirX;
@@ -907,6 +1359,11 @@ namespace ZL
v.PositionData = { a, b, c };
Vector3f rgb{ enemyColor.x(), enemyColor.y(), enemyColor.z() };
v.ColorData = { rgb, rgb, rgb };
+ // defaultColor vertex shader expects vNormal and vTexCoord (avoids NaN on WebGL).
+ const Vector3f n{ 0.f, 0.f, 1.f };
+ v.NormalData = { n, n, n };
+ const Vector2f uv{ 0.f, 0.f };
+ v.TexCoordData = { uv, uv, uv };
hudTempMesh.AssignFrom(v);
renderer.DrawVertexRenderStruct(hudTempMesh);
};
@@ -923,27 +1380,22 @@ namespace ZL
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
renderer.shaderManager.PushShader("defaultColor");
- renderer.PushProjectionMatrix((float)Environment::width, (float)Environment::height, 0.f, 1.f);
+ renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, 0.f, 1.f);
renderer.PushMatrix();
renderer.LoadIdentity();
- // треугольник-стрелка
drawTri(tip, left, right);
- // “хвост” (короткая черта)
float tailLen = 14.0f;
float tailX = edgeX - dirX * 6.0f;
float tailY = edgeY - dirY * 6.0f;
- // хвост рисуем как тонкий прямоугольник, ориентированный примерно по направлению:
- // (упрощение: горизонт/вертикаль не поворачиваем, но выглядит ок. Хочешь — сделаем поворот матрицей)
+
drawBar(tailX, tailY, max(thickness, tailLen), thickness);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader();
- // дистанция рядом со стрелкой
- // (у тебя ещё будет “статично под прицелом” — это просто другой TextView / drawText)
{
std::string d = std::to_string((int)dist) + "m";
float tx = edgeX + px * 18.0f;
@@ -1081,6 +1533,10 @@ namespace ZL
for (auto const& [id, remotePlayer] : latestRemotePlayers) {
+ if (networkClient && id == networkClient->GetClientId()) {
+ continue;
+ }
+
if (!remotePlayer.canFetchClientStateAtTime(nowRoundedWithDelay))
{
continue;
@@ -1089,7 +1545,6 @@ namespace ZL
ClientState playerState = remotePlayer.fetchClientStateAtTime(nowRoundedWithDelay);
remotePlayerStates[id] = playerState;
-
}
for (auto& p : projectiles) {
@@ -1098,22 +1553,25 @@ namespace ZL
}
}
- std::vector projCameraPoints;
+
for (const auto& p : projectiles) {
if (p && p->isActive()) {
Vector3f worldPos = p->getPosition();
Vector3f rel = worldPos - Environment::shipState.position;
Vector3f camPos = Environment::inverseShipMatrix * rel;
- projCameraPoints.push_back(camPos);
+ p->projectileEmitter.setEmissionPoints({ camPos });
+ p->projectileEmitter.emit();
+ p->projectileEmitter.update(static_cast(delta));
}
}
+ /*
if (!projCameraPoints.empty()) {
projectileEmitter.setEmissionPoints(projCameraPoints);
projectileEmitter.emit();
}
else {
projectileEmitter.setEmissionPoints(std::vector());
- }
+ }*/
std::vector shipCameraPoints;
for (const auto& lp : shipLocalEmissionPoints) {
@@ -1125,7 +1583,7 @@ namespace ZL
}
sparkEmitter.update(static_cast(delta));
- projectileEmitter.update(static_cast(delta));
+ //projectileEmitter.update(static_cast(delta));
explosionEmitter.update(static_cast(delta));
if (showExplosion) {
@@ -1156,7 +1614,7 @@ namespace ZL
std::cerr << "GAME OVER: collision with planet (moved back and exploded)\n";
- menuManager.showGameOver();
+ menuManager.showGameOver(this->playerScore);
}
else {
bool stoneCollided = false;
@@ -1231,7 +1689,7 @@ namespace ZL
planetObject.planetStones.statuses[collidedTriIdx] = ChunkStatus::Empty;
}
- menuManager.showGameOver();
+ menuManager.showGameOver(this->playerScore);
}
}
}
@@ -1254,8 +1712,8 @@ namespace ZL
Vector3f{ 1.5f, 0.9f - 6.f, 5.0f }
};
- const float projectileSpeed = 60.0f;
- const float lifeMs = 5000.0f;
+ const float projectileSpeed = PROJECTILE_VELOCITY;
+ const float lifeMs = PROJECTILE_LIFE;
const float size = 0.5f;
Vector3f localForward = { 0,0,-1 };
@@ -1268,6 +1726,7 @@ namespace ZL
for (auto& p : projectiles) {
if (!p->isActive()) {
p->init(worldPos, worldVel, lifeMs, size, projectileTexture, renderer);
+ p->projectileEmitter = SparkEmitter(projectileEmitter);
break;
}
}
@@ -1279,8 +1738,8 @@ namespace ZL
if (networkClient) {
auto pending = networkClient->getPendingProjectiles();
if (!pending.empty()) {
- const float projectileSpeed = 60.0f;
- const float lifeMs = 5000.0f;
+ const float projectileSpeed = PROJECTILE_VELOCITY;
+ const float lifeMs = PROJECTILE_LIFE;
const float size = 0.5f;
for (const auto& pi : pending) {
const std::vector localOffsets = {
@@ -1305,6 +1764,7 @@ namespace ZL
for (auto& p : projectiles) {
if (!p->isActive()) {
p->init(shotPos, baseVel, lifeMs, size, projectileTexture, renderer);
+ p->projectileEmitter = SparkEmitter(projectileEmitter);
break;
}
}
@@ -1335,12 +1795,17 @@ namespace ZL
shipAlive = false;
gameOver = true;
Environment::shipState.velocity = 0.0f;
- menuManager.showGameOver();
+ menuManager.showGameOver(this->playerScore);
}
else {
deadRemotePlayers.insert(d.targetId);
std::cout << "Marked remote player " << d.targetId << " as dead" << std::endl;
}
+ if (d.killerId == localId) {
+ this->playerScore += 1;
+ std::cout << "Client: Increased local score to " << this->playerScore << std::endl;
+
+ }
}
}
diff --git a/src/Space.h b/src/Space.h
index b1f8fed..b38b98e 100644
--- a/src/Space.h
+++ b/src/Space.h
@@ -78,6 +78,9 @@ namespace ZL {
VertexDataStruct spaceshipBase;
VertexRenderStruct spaceship;
+ std::shared_ptr cargoTexture;
+ VertexDataStruct cargoBase;
+ VertexRenderStruct cargo;
VertexRenderStruct cubemap;
@@ -93,7 +96,7 @@ namespace ZL {
std::shared_ptr projectileTexture;
float projectileCooldownMs = 500.0f;
int64_t lastProjectileFireTime = 0;
- int maxProjectiles = 32;
+ int maxProjectiles = 500;
std::vector shipLocalEmissionPoints;
@@ -119,6 +122,10 @@ namespace ZL {
static constexpr float CLOSE_DIST = 600.0f;
std::unordered_set deadRemotePlayers;
+ int playerScore = 0;
+
+ static constexpr float TARGET_MAX_DIST = 50000.0f;
+ static constexpr float TARGET_MAX_DIST_SQ = TARGET_MAX_DIST * TARGET_MAX_DIST;
// --- Target HUD (brackets + offscreen arrow) ---
int trackedTargetId = -1;
@@ -130,9 +137,45 @@ namespace ZL {
// helpers
void drawTargetHud(); // рисует рамку или стрелку
- int pickTargetId() const; // выбирает цель (пока: ближайший живой удаленный игрок)
+ int pickTargetId() const; // ???????? ???? (????: ????????? ????? ????????? ?????)
void clearTextRendererCache();
+
+ // Crosshair HUD
+ struct CrosshairConfig {
+ bool enabled = true;
+ int refW = 1280;
+ int refH = 720;
+
+ float scaleMul = 1.0f;
+
+ Eigen::Vector3f color = { 1.f, 1.f, 1.f };
+ float alpha = 1.0f; // cl_crosshairalpha
+ float thicknessPx = 2.0f; // cl_crosshairthickness
+ float gapPx = 10.0f;
+
+ float topLenPx = 14.0f;
+ float topAngleDeg = 90.0f;
+
+ struct Arm { float lenPx; float angleDeg; };
+ std::vector arms;
+ };
+
+ CrosshairConfig crosshairCfg;
+ bool crosshairCfgLoaded = false;
+
+ // кеш геометрии
+ VertexRenderStruct crosshairMesh;
+ bool crosshairMeshValid = false;
+ int crosshairLastW = 0, crosshairLastH = 0;
+ float crosshairLastAlpha = -1.0f;
+ float crosshairLastThickness = -1.0f;
+ float crosshairLastGap = -1.0f;
+ float crosshairLastScaleMul = -1.0f;
+
+ bool loadCrosshairConfig(const std::string& path);
+ void rebuildCrosshairMeshIfNeeded();
+ void drawCrosshair();
};
diff --git a/src/SparkEmitter.cpp b/src/SparkEmitter.cpp
index 05f686c..5f70710 100644
--- a/src/SparkEmitter.cpp
+++ b/src/SparkEmitter.cpp
@@ -25,6 +25,23 @@ namespace ZL {
sparkQuad.data = VertexDataStruct();
}
+ SparkEmitter::SparkEmitter(const SparkEmitter& copyFrom)
+ : particles(copyFrom.particles), emissionPoints(copyFrom.emissionPoints),
+ lastEmissionTime(copyFrom.lastEmissionTime), emissionRate(copyFrom.emissionRate),
+ isActive(copyFrom.isActive), drawPositions(copyFrom.drawPositions),
+ drawTexCoords(copyFrom.drawTexCoords), drawDataDirty(copyFrom.drawDataDirty),
+ sparkQuad(copyFrom.sparkQuad), texture(copyFrom.texture),
+ maxParticles(copyFrom.maxParticles), particleSize(copyFrom.particleSize),
+ biasX(copyFrom.biasX), speedRange(copyFrom.speedRange),
+ zSpeedRange(copyFrom.zSpeedRange),
+ scaleRange(copyFrom.scaleRange),
+ lifeTimeRange(copyFrom.lifeTimeRange),
+ shaderProgramName(copyFrom.shaderProgramName),
+ configured(copyFrom.configured), useWorldSpace(copyFrom.useWorldSpace)
+ {
+ }
+
+
SparkEmitter::SparkEmitter(const std::vector& positions, float rate)
: emissionPoints(positions), emissionRate(rate), isActive(true),
drawDataDirty(true), maxParticles(positions.size() * 100),
diff --git a/src/SparkEmitter.h b/src/SparkEmitter.h
index 3eed0aa..812b828 100644
--- a/src/SparkEmitter.h
+++ b/src/SparkEmitter.h
@@ -41,7 +41,7 @@ namespace ZL {
float biasX;
// Ranges (used when config supplies intervals)
- struct FloatRange { float min; float max; };
+ struct FloatRange { float min=0; float max=0; };
FloatRange speedRange; // XY speed
FloatRange zSpeedRange; // Z speed
FloatRange scaleRange;
@@ -55,6 +55,7 @@ namespace ZL {
public:
SparkEmitter();
+ SparkEmitter(const SparkEmitter& copyFrom);
SparkEmitter(const std::vector& positions, float rate = 100.0f);
SparkEmitter(const std::vector& positions,
std::shared_ptr tex,
diff --git a/src/UiManager.cpp b/src/UiManager.cpp
index d051f8c..f06e47f 100644
--- a/src/UiManager.cpp
+++ b/src/UiManager.cpp
@@ -184,21 +184,89 @@ namespace ZL {
std::shared_ptr parseNode(const json& j, Renderer& renderer, const std::string& zipFile) {
auto node = std::make_shared();
- if (j.contains("type") && j["type"].is_string()) node->type = j["type"].get();
- if (j.contains("name") && j["name"].is_string()) node->name = j["name"].get();
- if (j.contains("x")) node->rect.x = j["x"].get();
- if (j.contains("y")) node->rect.y = j["y"].get();
- if (j.contains("width")) node->rect.w = j["width"].get();
- if (j.contains("height")) node->rect.h = j["height"].get();
+ // 1. Определяем тип контейнера и ориентацию
+ std::string typeStr = j.value("type", "FrameLayout"); // По умолчанию FrameLayout
+ if (typeStr == "LinearLayout") {
+ node->layoutType = LayoutType::Linear;
+ }
+ else {
+ node->layoutType = LayoutType::Frame;
+ }
- if (j.contains("orientation") && j["orientation"].is_string()) node->orientation = j["orientation"].get();
- if (j.contains("spacing")) node->spacing = j["spacing"].get();
+ if (j.contains("name")) node->name = j["name"].get();
- if (node->type == "Button") {
+ // 2. Читаем размеры во временные "локальные" поля
+ // Это критически важно: мы не пишем сразу в screenRect,
+ // так как LinearLayout их пересчитает.
+ node->localX = j.value("x", 0.0f);
+ node->localY = j.value("y", 0.0f);
+ if (j.contains("width")) {
+ if (j["width"].is_string() && j["width"] == "match_parent") {
+ node->width = -1.0f; // Наш маркер для match_parent
+ }
+ else {
+ node->width = j["width"].get();
+ }
+ }
+ else
+ {
+ node->width = 0.0f;
+ }
+ if (j.contains("height")) {
+ if (j["height"].is_string() && j["height"] == "match_parent") {
+ node->height = -1.0f; // Наш маркер для match_parent
+ }
+ else {
+ node->height = j["height"].get();
+ }
+ }
+ else
+ {
+ node->height = 0.0f;
+ }
+
+ // 3. Параметры компоновки
+ if (j.contains("orientation")) {
+ std::string orient = j["orientation"].get();
+ node->orientation = (orient == "horizontal") ? Orientation::Horizontal : Orientation::Vertical;
+ }
+ node->spacing = j.value("spacing", 0.0f);
+
+ if (j.contains("horizontal_align")) {
+ std::string halign = j["horizontal_align"];
+ if (halign == "center") node->layoutSettings.hAlign = HorizontalAlign::Center;
+ else if (halign == "right") node->layoutSettings.hAlign = HorizontalAlign::Right;
+ }
+
+ if (j.contains("vertical_align")) {
+ std::string valign = j["vertical_align"];
+ if (valign == "center") node->layoutSettings.vAlign = VerticalAlign::Center;
+ else if (valign == "bottom") node->layoutSettings.vAlign = VerticalAlign::Bottom;
+ }
+
+ if (j.contains("horizontal_gravity")) {
+ std::string hg = j["horizontal_gravity"].get();
+ if (hg == "right") node->layoutSettings.hGravity = HorizontalGravity::Right;
+ else node->layoutSettings.hGravity = HorizontalGravity::Left;
+ }
+
+ // Читаем Vertical Gravity
+ if (j.contains("vertical_gravity")) {
+ std::string vg = j["vertical_gravity"].get();
+ if (vg == "bottom") node->layoutSettings.vGravity = VerticalGravity::Bottom;
+ else node->layoutSettings.vGravity = VerticalGravity::Top;
+ }
+
+ // Подготавливаем базовый rect для компонентов (кнопок и т.д.)
+ // На этапе парсинга мы даем им "желаемый" размер
+ UiRect initialRect = { node->localX, node->localY, node->width, node->height };
+
+
+ if (typeStr == "Button") {
auto btn = std::make_shared();
btn->name = node->name;
- btn->rect = node->rect;
+ btn->rect = initialRect;
if (!j.contains("textures") || !j["textures"].is_object()) {
std::cerr << "UiManager: Button '" << btn->name << "' missing textures" << std::endl;
@@ -225,10 +293,10 @@ namespace ZL {
node->button = btn;
}
- else if (node->type == "Slider") {
+ else if (typeStr == "Slider") {
auto s = std::make_shared();
s->name = node->name;
- s->rect = node->rect;
+ s->rect = initialRect;
if (!j.contains("textures") || !j["textures"].is_object()) {
std::cerr << "UiManager: Slider '" << s->name << "' missing textures" << std::endl;
@@ -261,10 +329,10 @@ namespace ZL {
node->slider = s;
}
- else if (node->type == "TextField") {
+ else if (typeStr == "TextField") {
auto tf = std::make_shared();
tf->name = node->name;
- tf->rect = node->rect;
+ tf->rect = initialRect;
if (j.contains("placeholder")) tf->placeholder = j["placeholder"].get();
if (j.contains("fontPath")) tf->fontPath = j["fontPath"].get();
@@ -331,11 +399,11 @@ namespace ZL {
}
}
- if (node->type == "TextView") {
+ if (typeStr == "TextView") {
auto tv = std::make_shared();
- tv->name = node->name;
- tv->rect = node->rect;
+ tv->name = node->name;
+ tv->rect = initialRect;
if (j.contains("text")) tv->text = j["text"].get();
if (j.contains("fontPath")) tv->fontPath = j["fontPath"].get();
if (j.contains("fontSize")) tv->fontSize = j["fontSize"].get();
@@ -400,6 +468,7 @@ namespace ZL {
throw std::runtime_error("Failed to load UI file: " + path);
}
+
root = parseNode(j["root"], renderer, zipFile);
return root;
@@ -407,7 +476,14 @@ namespace ZL {
void UiManager::replaceRoot(std::shared_ptr newRoot) {
root = newRoot;
- layoutNode(root);
+ layoutNode(
+ root,
+ 0.0f, 0.0f, // parentX, parentY (экран начинается с 0,0)
+ Environment::projectionWidth, // parentW
+ Environment::projectionHeight, // parentH
+ root->localX, // finalLocalX
+ root->localY // finalLocalY
+ );
buttons.clear();
sliders.clear();
textViews.clear();
@@ -431,39 +507,170 @@ namespace ZL {
replaceRoot(newRoot);
}
-
- void UiManager::layoutNode(const std::shared_ptr& node) {
- for (auto& child : node->children) {
- child->rect.x += node->rect.x;
- child->rect.y += node->rect.y;
- }
+ void UiManager::layoutNode(const std::shared_ptr& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY) {
- if (node->type == "LinearLayout") {
- std::string orient = node->orientation;
- std::transform(orient.begin(), orient.end(), orient.begin(), ::tolower);
+ node->screenRect.w = (node->width < 0) ? parentW : node->width;
+ node->screenRect.h = (node->height < 0) ? parentH : node->height;
- float cursorX = node->rect.x;
- float cursorY = node->rect.y;
- for (auto& child : node->children) {
- if (orient == "horizontal") {
- child->rect.x = cursorX;
- child->rect.y = node->rect.y;
- cursorX += child->rect.w + node->spacing;
+ // ТЕПЕРЬ используем переданные координаты, а не node->localX напрямую
+ node->screenRect.x = parentX + finalLocalX;
+ node->screenRect.y = parentY + finalLocalY;
+
+ float currentW = node->screenRect.w;
+ float currentH = node->screenRect.h;
+
+ if (node->layoutType == LayoutType::Linear) {
+ float totalContentWidth = 0;
+ float totalContentHeight = 0;
+
+ // Предварительный расчет занимаемого места всеми детьми
+ for (size_t i = 0; i < node->children.size(); ++i) {
+ if (node->orientation == Orientation::Vertical) {
+ totalContentHeight += node->children[i]->height;
+ if (i < node->children.size() - 1) totalContentHeight += node->spacing;
}
else {
- child->rect.x = node->rect.x;
- child->rect.y = cursorY;
- cursorY += child->rect.h + node->spacing;
+ totalContentWidth += node->children[i]->width;
+ if (i < node->children.size() - 1) totalContentWidth += node->spacing;
}
- layoutNode(child);
+ }
+
+ float startX = 0;
+ float startY = currentH;
+
+ if (node->orientation == Orientation::Vertical) {
+ if (node->layoutSettings.vAlign == VerticalAlign::Center) {
+ startY = (currentH + totalContentHeight) / 2.0f;
+ }
+ else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) {
+ startY = totalContentHeight;
+ }
+ }
+
+ // Горизонтальное выравнивание всего блока
+ if (node->orientation == Orientation::Horizontal) {
+ if (node->layoutSettings.hAlign == HorizontalAlign::Center) {
+ startX = (currentW - totalContentWidth) / 2.0f;
+ }
+ else if (node->layoutSettings.hAlign == HorizontalAlign::Right) {
+ startX = currentW - totalContentWidth;
+ }
+ }
+
+ float cursorX = startX;
+ float cursorY = startY;
+
+ for (auto& child : node->children) {
+
+ float childW = (child->width < 0) ? currentW : child->width;
+ float childH = (child->height < 0) ? currentH : child->height;
+
+ if (node->orientation == Orientation::Vertical) {
+ cursorY -= childH; // используем вычисленный childH
+
+ float childX = 0;
+ float freeSpaceX = currentW - childW;
+ if (node->layoutSettings.hAlign == HorizontalAlign::Center) childX = freeSpaceX / 2.0f;
+ else if (node->layoutSettings.hAlign == HorizontalAlign::Right) childX = freeSpaceX;
+
+ child->localX = childX;
+ child->localY = cursorY;
+ cursorY -= node->spacing;
+ }
+ else {
+ child->localX = cursorX;
+
+ // Вертикальное выравнивание внутри "строки" (Cross-axis alignment)
+ float childY = 0;
+ float freeSpaceY = currentH - childH;
+
+ if (node->layoutSettings.vAlign == VerticalAlign::Center) {
+ childY = freeSpaceY / 2.0f;
+ }
+ else if (node->layoutSettings.vAlign == VerticalAlign::Top) {
+ childY = freeSpaceY; // Прижимаем к верхнему краю (т.к. Y растет вверх)
+ }
+ else if (node->layoutSettings.vAlign == VerticalAlign::Bottom) {
+ childY = 0; // Прижимаем к нижнему краю
+ }
+
+ child->localY = childY;
+
+ // Сдвигаем курсор вправо для следующего элемента
+ cursorX += childW + node->spacing;
+ }
+ layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, child->localX, child->localY);
}
}
else {
for (auto& child : node->children) {
- layoutNode(child);
+ float childW = (child->width < 0) ? currentW : child->width;
+ float childH = (child->height < 0) ? currentH : child->height;
+
+ float fLX = child->localX;
+ float fLY = child->localY;
+
+ if (child->layoutSettings.hGravity == HorizontalGravity::Right) {
+ fLX = currentW - childW - child->localX;
+ }
+ if (child->layoutSettings.vGravity == VerticalGravity::Top) {
+ fLY = currentH - childH - child->localY;
+ }
+
+ // Передаем рассчитанные fLX, fLY в рекурсию
+ layoutNode(child, node->screenRect.x, node->screenRect.y, currentW, currentH, fLX, fLY);
}
}
+
+ // Обновляем меши визуальных компонентов
+ syncComponentRects(node);
+ }
+
+ void UiManager::syncComponentRects(const std::shared_ptr& node) {
+ if (!node) return;
+
+ // 1. Обновляем кнопку
+ if (node->button) {
+ node->button->rect = node->screenRect;
+ // Если у кнопки есть анимационные смещения, они учитываются внутри buildMesh
+ // или при рендеринге через Uniform-переменные матрицы модели.
+ node->button->buildMesh();
+ }
+
+ // 2. Обновляем слайдер
+ if (node->slider) {
+ node->slider->rect = node->screenRect;
+ node->slider->buildTrackMesh();
+ node->slider->buildKnobMesh();
+ }
+
+ // 3. Обновляем текстовое поле (TextView)
+ if (node->textView) {
+ node->textView->rect = node->screenRect;
+ // Если в TextView реализован кэш меша для текста, его нужно обновить здесь
+ // node->textView->rebuildText();
+ }
+
+ // 4. Обновляем поле ввода (TextField)
+ if (node->textField) {
+ node->textField->rect = node->screenRect;
+ // Аналогично для курсора и фонового меша
+ }
+ }
+
+ void UiManager::updateAllLayouts() {
+ if (!root) return;
+
+ // Запускаем расчет от корня, передавая размеры экрана как "родительские"
+ layoutNode(
+ root,
+ 0.0f, 0.0f, // parentX, parentY (экран начинается с 0,0)
+ Environment::projectionWidth, // parentW
+ Environment::projectionHeight, // parentH
+ root->localX, // finalLocalX
+ root->localY // finalLocalY
+ );
}
void UiManager::collectButtonsAndSliders(const std::shared_ptr& node) {
@@ -657,7 +864,7 @@ namespace ZL {
}
void UiManager::draw(Renderer& renderer) {
- renderer.PushProjectionMatrix(Environment::width, Environment::height, -1, 1);
+ renderer.PushProjectionMatrix(Environment::projectionWidth, Environment::projectionHeight, -1, 1);
renderer.PushMatrix();
renderer.LoadIdentity();
@@ -906,22 +1113,28 @@ namespace ZL {
}
void UiManager::onMouseUp(int x, int y) {
+ std::vector> clicked;
+
for (auto& b : buttons) {
+ if (!b) continue;
bool contains = b->rect.contains((float)x, (float)y);
+
if (b->state == ButtonState::Pressed) {
if (contains && pressedButton == b) {
- if (b->onClick) {
- b->onClick(b->name);
- }
+ clicked.push_back(b);
}
b->state = contains ? ButtonState::Hover : ButtonState::Normal;
}
}
- pressedButton.reset();
- if (pressedSlider) {
- pressedSlider.reset();
+ for (auto& b : clicked) {
+ if (b->onClick) {
+ b->onClick(b->name);
+ }
}
+
+ pressedButton.reset();
+ if (pressedSlider) pressedSlider.reset();
}
void UiManager::onKeyPress(unsigned char key) {
diff --git a/src/UiManager.h b/src/UiManager.h
index 05af33c..59743d2 100644
--- a/src/UiManager.h
+++ b/src/UiManager.h
@@ -31,6 +31,48 @@ namespace ZL {
Pressed
};
+ enum class LayoutType {
+ Frame, // Позиционирование по X, Y
+ Linear // Автоматическое позиционирование
+ };
+
+ enum class Orientation {
+ Vertical,
+ Horizontal
+ };
+
+ enum class HorizontalAlign {
+ Left,
+ Center,
+ Right
+ };
+
+ enum class VerticalAlign {
+ Top,
+ Center,
+ Bottom
+ };
+
+ enum class HorizontalGravity {
+ Left,
+ Right
+ };
+
+ enum class VerticalGravity {
+ Bottom, // Обычно в OpenGL Y растет вверх, так что низ - это 0
+ Top
+ };
+
+
+ // В структуру или класс, отвечающий за LinearLayout (вероятно, это свойства UiNode)
+ struct LayoutSettings {
+ HorizontalAlign hAlign = HorizontalAlign::Left;
+ VerticalAlign vAlign = VerticalAlign::Top;
+
+ HorizontalGravity hGravity = HorizontalGravity::Left;
+ VerticalGravity vGravity = VerticalGravity::Top;
+ };
+
struct UiButton {
std::string name;
UiRect rect;
@@ -111,21 +153,38 @@ namespace ZL {
};
struct UiNode {
- std::string type;
- UiRect rect;
std::string name;
+ LayoutType layoutType = LayoutType::Frame;
+ Orientation orientation = Orientation::Vertical;
+ float spacing = 0.0f;
+
+ LayoutSettings layoutSettings;
+
+ // Внутренние вычисленные координаты для OpenGL
+ // Именно их мы передаем в Vertex Buffer при buildMesh()
+ UiRect screenRect;
+
+ // Данные из JSON (желаемые размеры и смещения)
+ float localX = 0;
+ float localY = 0;
+ float width = 0;
+ float height = 0;
+
+ // Иерархия
std::vector> children;
+
+ // Компоненты (только один из них обычно активен для ноды)
std::shared_ptr button;
std::shared_ptr slider;
std::shared_ptr textView;
std::shared_ptr textField;
- std::string orientation = "vertical";
- float spacing = 0.0f;
+ // Анимации
struct AnimStep {
std::string type;
float toX = 0.0f;
float toY = 0.0f;
+ float toScale = 1.0f; // Полезно добавить для UI
float durationMs = 0.0f;
std::string easing = "linear";
};
@@ -200,9 +259,11 @@ namespace ZL {
bool startAnimationOnNode(const std::string& nodeName, const std::string& animName);
bool stopAnimationOnNode(const std::string& nodeName, const std::string& animName);
bool setAnimationCallback(const std::string& nodeName, const std::string& animName, std::function cb);
+ void updateAllLayouts();
private:
- void layoutNode(const std::shared_ptr& node);
+ void layoutNode(const std::shared_ptr& node, float parentX, float parentY, float parentW, float parentH, float finalLocalX, float finalLocalY);
+ void syncComponentRects(const std::shared_ptr& node);
void collectButtonsAndSliders(const std::shared_ptr& node);
struct ActiveAnim {
diff --git a/src/main.cpp b/src/main.cpp
index 57e0b5e..6b62092 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -5,15 +5,95 @@
#include
#endif
+#ifdef EMSCRIPTEN
+#include
+#include
+#endif
+
+// For Emscripten the game is heap-allocated so it can be destroyed and
+// re-created when the WebGL context is lost and restored (e.g. fullscreen).
+// For Android and Desktop a plain global value is used (no context loss).
+#ifdef EMSCRIPTEN
+ZL::Game* g_game = nullptr;
+#else
ZL::Game game;
+#endif
void MainLoop() {
+#ifdef EMSCRIPTEN
+ if (g_game) g_game->update();
+#else
game.update();
+#endif
}
+#ifdef EMSCRIPTEN
+
+EM_BOOL onWebGLContextLost(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) {
+ delete g_game;
+ g_game = nullptr;
+ return EM_TRUE;
+}
+
+EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void* /*userData*/) {
+ g_game = new ZL::Game();
+ g_game->setup();
+ return EM_TRUE;
+}
+
+static void applyResize(int logicalW, int logicalH) {
+ // Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung)
+ double dpr = emscripten_get_device_pixel_ratio();
+
+ // Вычисляем реальные физические пиксели
+ int physicalW = static_cast(logicalW * dpr);
+ int physicalH = static_cast(logicalH * dpr);
+
+ // Устанавливаем размер внутреннего буфера канваса
+ emscripten_set_canvas_element_size("#canvas", physicalW, physicalH);
+
+ // Сообщаем SDL о новом размере.
+ // ВАЖНО: SDL2 в Emscripten ожидает здесь именно физические пиксели
+ // для корректной работы последующих вызовов glViewport.
+ if (ZL::Environment::window) {
+ SDL_SetWindowSize(ZL::Environment::window, physicalW, physicalH);
+ }
+
+ // Обновляем ваши внутренние переменные окружения
+ ZL::Environment::width = physicalW;
+ ZL::Environment::height = physicalH;
+
+ // Пушим событие, чтобы движок пересчитал матрицы проекции
+ SDL_Event e = {};
+ e.type = SDL_WINDOWEVENT;
+ e.window.event = SDL_WINDOWEVENT_RESIZED;
+ e.window.data1 = physicalW;
+ e.window.data2 = physicalH;
+ SDL_PushEvent(&e);
+}
+
+EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) {
+ // Use the event's window dimensions — querying the canvas element would
+ // return its old fixed size (e.g. 1280x720) before it has been resized.
+ applyResize(e->windowInnerWidth, e->windowInnerHeight);
+ return EM_FALSE;
+}
+
+EM_BOOL onFullscreenChanged(int /*eventType*/, const EmscriptenFullscreenChangeEvent* e, void* /*userData*/) {
+ // Вместо window.innerWidth, попробуйте запросить размер целевого элемента
+ // так как после перехода в FS именно он растягивается на весь экран.
+ double clientW, clientH;
+ emscripten_get_element_css_size("#canvas", &clientW, &clientH);
+ applyResize(clientW, clientH);
+ return EM_FALSE;
+}
+
+#endif
+
+
#ifdef __ANDROID__
extern "C" int SDL_main(int argc, char* argv[]) {
@@ -34,7 +114,7 @@ extern "C" int SDL_main(int argc, char* argv[]) {
__android_log_print(ANDROID_LOG_INFO, "Game", "Display resolution: %dx%d",
ZL::Environment::width, ZL::Environment::height);
-
+
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
@@ -52,13 +132,13 @@ extern "C" int SDL_main(int argc, char* argv[]) {
ZL::Environment::width, ZL::Environment::height,
SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
);
-
+
if (!ZL::Environment::window) {
__android_log_print(ANDROID_LOG_ERROR, "Game", "Failed to create window: %s", SDL_GetError());
SDL_Quit();
return 1;
}
-
+
SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window);
if (!ctx) {
__android_log_print(ANDROID_LOG_ERROR, "Game", "SDL_GL_CreateContext failed: %s", SDL_GetError());
@@ -94,7 +174,7 @@ extern "C" int SDL_main(int argc, char* argv[]) {
}
return 0;
-
+
}
@@ -103,8 +183,8 @@ extern "C" int SDL_main(int argc, char* argv[]) {
int main(int argc, char *argv[]) {
try
{
-
-
+
+
constexpr int CONST_WIDTH = 1280;
constexpr int CONST_HEIGHT = 720;
@@ -142,6 +222,35 @@ int main(int argc, char *argv[]) {
SDL_GL_MakeCurrent(win, glContext);
ZL::Environment::window = win;
+
+ g_game = new ZL::Game();
+ g_game->setup();
+
+ // Re-create the game object when the WebGL context is lost and restored
+ // (this happens e.g. when the user toggles fullscreen in the browser).
+ emscripten_set_webglcontextlost_callback("#canvas", nullptr, EM_TRUE, onWebGLContextLost);
+ emscripten_set_webglcontextrestored_callback("#canvas", nullptr, EM_TRUE, onWebGLContextRestored);
+
+ // Keep Environment::width/height in sync when the canvas is resized.
+ emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized);
+ emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_FALSE, onFullscreenChanged);
+
+ // 2. ИНИЦИАЛИЗАЦИЯ РАЗМЕРОВ:
+ // Получаем реальные размеры окна браузера на момент запуска
+ int canvasW = EM_ASM_INT({ return window.innerWidth; });
+ int canvasH = EM_ASM_INT({ return window.innerHeight; });
+
+ // Вызываем вашу функцию — она сама применит DPR, выставит физический размер
+ // канваса и отправит SDL_WINDOWEVENT_RESIZED для настройки проекции.
+ applyResize(canvasW, canvasH);
+
+ // 3. Создаем игру и вызываем setup (теперь проекция уже будет знать верный size)
+ g_game = new ZL::Game();
+ g_game->setup();
+
+ SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
+
+ emscripten_set_main_loop(MainLoop, 0, 1);
#else
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
SDL_Log("SDL init failed: %s", SDL_GetError());
@@ -161,13 +270,9 @@ int main(int argc, char *argv[]) {
SDL_GLContext ctx = SDL_GL_CreateContext(ZL::Environment::window);
SDL_GL_MakeCurrent(ZL::Environment::window, ctx);
-#endif
game.setup();
-#ifdef EMSCRIPTEN
- emscripten_set_main_loop(MainLoop, 0, 1);
-#else
while (!game.shouldExit()) {
game.update();
SDL_Delay(2);
@@ -182,4 +287,4 @@ int main(int argc, char *argv[]) {
return 0;
}
-#endif
\ No newline at end of file
+#endif
diff --git a/src/network/ClientState.cpp b/src/network/ClientState.cpp
index 578f536..e33c965 100644
--- a/src/network/ClientState.cpp
+++ b/src/network/ClientState.cpp
@@ -1,4 +1,4 @@
-#include "ClientState.h"
+#include "ClientState.h"
uint32_t fnv1a_hash(const std::string& data) {
uint32_t hash = 0x811c9dc5;
@@ -169,7 +169,7 @@ void ClientState::apply_lag_compensation(std::chrono::system_clock::time_point n
while (deltaMsLeftover > 0)
{
- long long miniDelta = 50;
+ long long miniDelta = std::min(50LL, deltaMsLeftover);
simulate_physics(miniDelta);
deltaMsLeftover -= miniDelta;
}
@@ -207,7 +207,7 @@ void ClientState::handle_full_sync(const std::vector& parts, int st
discreteAngle = std::stoi(parts[startFrom + 13]);
}
-std::string ClientState::formPingMessageContent()
+std::string ClientState::formPingMessageContent() const
{
Eigen::Quaternionf q(rotation);
diff --git a/src/network/ClientState.h b/src/network/ClientState.h
index 7e48953..5bbd8b3 100644
--- a/src/network/ClientState.h
+++ b/src/network/ClientState.h
@@ -1,9 +1,10 @@
-#pragma once
+#pragma once
#include
#include
#define _USE_MATH_DEFINES
#include
#include
+#include
using std::min;
@@ -13,12 +14,12 @@ constexpr auto NET_SECRET = "880b3713b9ff3e7a94b2712d54679e1f";
#define ENABLE_NETWORK_CHECKSUM
constexpr float ANGULAR_ACCEL = 0.005f * 1000.0f;
-constexpr float SHIP_ACCEL = 1.0f * 1000.0f;
+constexpr float SHIP_ACCEL = 1.0f * 1000.0f;
constexpr float ROTATION_SENSITIVITY = 0.002f;
constexpr float PLANET_RADIUS = 20000.f;
constexpr float PLANET_ALIGN_ZONE = 1.05f;
-constexpr float PLANET_ANGULAR_ACCEL = 0.01f; // Подбери под динамику
+constexpr float PLANET_ANGULAR_ACCEL = 0.01f;
constexpr float PLANET_MAX_ANGULAR_VELOCITY = 10.f;
constexpr float PITCH_LIMIT = static_cast(M_PI) / 9.f;//18.0f;
@@ -26,6 +27,14 @@ constexpr long long SERVER_DELAY = 0; //ms
constexpr long long CLIENT_DELAY = 500; //ms
constexpr long long CUTOFF_TIME = 5000; //ms
+constexpr float PROJECTILE_VELOCITY = 600.f;
+constexpr float PROJECTILE_LIFE = 15000.f; //ms
+
+const float projectileHitRadius = 1.5f * 5;
+const float boxCollisionRadius = 2.0f * 5;
+const float shipCollisionRadius = 15.0f * 5;
+const float npcCollisionRadius = 5.0f * 5;
+
uint32_t fnv1a_hash(const std::string& data);
struct ClientState {
@@ -38,7 +47,9 @@ struct ClientState {
float discreteMag = 0;
int discreteAngle = -1;
- // Для расчета лага
+ std::string nickname = "Player";
+ int shipType = 0;
+
std::chrono::system_clock::time_point lastUpdateServerTime;
void simulate_physics(size_t delta);
@@ -47,7 +58,7 @@ struct ClientState {
void handle_full_sync(const std::vector& parts, int startFrom);
- std::string formPingMessageContent();
+ std::string formPingMessageContent() const;
};
struct ClientStateInterval
diff --git a/src/network/LocalClient.cpp b/src/network/LocalClient.cpp
index 5336b43..1d43a78 100644
--- a/src/network/LocalClient.cpp
+++ b/src/network/LocalClient.cpp
@@ -1,4 +1,4 @@
-#include "LocalClient.h"
+#include "LocalClient.h"
#include
#include
#include
@@ -21,8 +21,8 @@ namespace ZL {
std::random_device rd;
std::mt19937 gen(rd());
- const float MIN_COORD = -100.0f;
- const float MAX_COORD = 100.0f;
+ const float MIN_COORD = -1000.0f;
+ const float MAX_COORD = 1000.0f;
const float MIN_DISTANCE = 3.0f;
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE;
const int MAX_ATTEMPTS = 1000;
@@ -68,7 +68,7 @@ namespace ZL {
Eigen::Vector3f LocalClient::generateRandomPosition() {
std::random_device rd;
std::mt19937 gen(rd());
- std::uniform_real_distribution<> distrib(-500.0, 500.0);
+ std::uniform_real_distribution<> distrib(-5000.0, 5000.0);
return Eigen::Vector3f(
(float)distrib(gen),
@@ -79,6 +79,10 @@ namespace ZL {
void LocalClient::initializeNPCs() {
npcs.clear();
+ std::random_device rd;
+ std::mt19937 gen(rd());
+ std::uniform_int_distribution typeDistrib(0, 1); // 0 = default ship, 1 = cargo
+
for (int i = 0; i < 3; ++i) {
LocalNPC npc;
npc.id = 100 + i;
@@ -91,6 +95,11 @@ namespace ZL {
npc.currentState.discreteAngle = -1;
npc.currentState.currentAngularVelocity = Eigen::Vector3f::Zero();
+// random
+ int shipType = typeDistrib(gen);
+ npc.shipType = shipType;
+ npc.currentState.shipType = shipType;
+
npc.targetPosition = generateRandomPosition();
npc.lastStateUpdateMs = std::chrono::duration_cast(
std::chrono::system_clock::now().time_since_epoch()).count();
@@ -229,11 +238,6 @@ namespace ZL {
auto now_ms = std::chrono::duration_cast(
std::chrono::system_clock::now().time_since_epoch()).count();
- const float projectileHitRadius = 1.5f;
- const float boxCollisionRadius = 2.0f;
- const float shipCollisionRadius = 15.0f;
- const float npcCollisionRadius = 5.0f;
-
std::vector> boxProjectileCollisions;
for (size_t bi = 0; bi < serverBoxes.size(); ++bi) {
diff --git a/src/network/LocalClient.h b/src/network/LocalClient.h
index c8707a4..438dede 100644
--- a/src/network/LocalClient.h
+++ b/src/network/LocalClient.h
@@ -1,4 +1,4 @@
-#pragma once
+#pragma once
#include "NetworkInterface.h"
#include
@@ -31,6 +31,7 @@ namespace ZL {
Eigen::Vector3f targetPosition;
uint64_t lastStateUpdateMs = 0;
bool destroyed = false;
+ int shipType = 0;
};
class LocalClient : public INetworkClient {
diff --git a/src/network/NetworkInterface.h b/src/network/NetworkInterface.h
index 74dbdad..2c7e704 100644
--- a/src/network/NetworkInterface.h
+++ b/src/network/NetworkInterface.h
@@ -41,6 +41,8 @@ namespace ZL {
virtual std::vector> getServerBoxes() = 0;
+ virtual std::vector getServerBoxDestroyedFlags() { return {}; }
+
virtual std::vector getPendingProjectiles() = 0;
virtual std::vector getPendingDeaths() = 0;
diff --git a/src/network/WebSocketClient.cpp b/src/network/WebSocketClient.cpp
index bdf0232..d19463a 100644
--- a/src/network/WebSocketClient.cpp
+++ b/src/network/WebSocketClient.cpp
@@ -1,4 +1,4 @@
-#ifdef NETWORK
+#ifdef NETWORK
#include "WebSocketClient.h"
#include
diff --git a/src/network/WebSocketClient.h b/src/network/WebSocketClient.h
index 5ccb45c..bd01ba2 100644
--- a/src/network/WebSocketClient.h
+++ b/src/network/WebSocketClient.h
@@ -1,4 +1,4 @@
-#pragma once
+#pragma once
#ifdef NETWORK
diff --git a/src/network/WebSocketClientBase.cpp b/src/network/WebSocketClientBase.cpp
index 48b99f2..6253f94 100644
--- a/src/network/WebSocketClientBase.cpp
+++ b/src/network/WebSocketClientBase.cpp
@@ -1,4 +1,4 @@
-#ifdef NETWORK
+#ifdef NETWORK
#include "WebSocketClientBase.h"
#include
@@ -41,36 +41,66 @@ namespace ZL {
return;
}
- // Обработка списка коробок от сервера
if (msg.rfind("BOXES:", 0) == 0) {
- std::string payload = msg.substr(6); // после "BOXES:"
- std::vector> parsedBoxes;
+ std::string payload = msg.substr(6);
+ std::vector> parsed;
if (!payload.empty()) {
auto items = split(payload, '|');
for (auto& item : items) {
- if (item.empty()) return;
+ if (item.empty()) continue;
auto parts = split(item, ':');
- if (parts.size() < 7) return;
+ if (parts.size() < 9) {
+ return;
+ }
try {
- float px = std::stof(parts[0]);
- float py = std::stof(parts[1]);
- float pz = std::stof(parts[2]);
+ int idx = std::stoi(parts[0]);
+ float px = std::stof(parts[1]);
+ float py = std::stof(parts[2]);
+ float pz = std::stof(parts[3]);
Eigen::Quaternionf q(
- std::stof(parts[3]),
std::stof(parts[4]),
std::stof(parts[5]),
- std::stof(parts[6])
+ std::stof(parts[6]),
+ std::stof(parts[7])
);
+ bool destroyed = (std::stoi(parts[8]) != 0);
+
Eigen::Matrix3f rot = q.toRotationMatrix();
- parsedBoxes.emplace_back(Eigen::Vector3f{ px, py, pz }, rot);
+ parsed.emplace_back(idx, Eigen::Vector3f{ px, py, pz }, rot, destroyed);
}
catch (...) {
- // пропускаем некорректную запись
return;
}
}
}
- serverBoxes_ = std::move(parsedBoxes);
+
+ int maxIdx = -1;
+ for (auto& t : parsed) {
+ int idx = std::get<0>(t);
+ if (idx > maxIdx) maxIdx = idx;
+ }
+ if (maxIdx < 0) {
+ serverBoxes_.clear();
+ serverBoxesDestroyed_.clear();
+ return;
+ }
+
+ serverBoxes_.clear();
+ serverBoxes_.resize((size_t)maxIdx + 1);
+ serverBoxesDestroyed_.clear();
+ serverBoxesDestroyed_.resize((size_t)maxIdx + 1, true);
+
+ for (auto& t : parsed) {
+ int idx = std::get<0>(t);
+ const Eigen::Vector3f& pos = std::get<1>(t);
+ const Eigen::Matrix3f& rot = std::get<2>(t);
+ bool destroyed = std::get<3>(t);
+ if (idx >= 0 && idx < serverBoxes_.size()) {
+ serverBoxes_[idx] = { pos, rot };
+ serverBoxesDestroyed_[idx] = destroyed;
+ }
+ }
+
return;
}
if (msg.rfind("RESPAWN_ACK:", 0) == 0) {
@@ -79,7 +109,6 @@ namespace ZL {
try {
int respawnedPlayerId = std::stoi(parts[1]);
pendingRespawns_.push_back(respawnedPlayerId);
- remotePlayers.erase(respawnedPlayerId);
std::cout << "Client: Received RESPAWN_ACK for player " << respawnedPlayerId << std::endl;
}
catch (...) {}
@@ -202,6 +231,11 @@ namespace ZL {
{
auto& rp = remotePlayers[remoteId];
+ if (!rp.timedStates.empty()) {
+ const ClientState& last = rp.timedStates.back();
+ remoteState.nickname = last.nickname;
+ remoteState.shipType = last.shipType;
+ }
rp.add_state(remoteState);
}
}
@@ -218,7 +252,7 @@ namespace ZL {
if (playerParts.size() < 15) return; // ID + 14 полей ClientState
int rId = std::stoi(playerParts[0]);
- if (rId == clientId) return; // Свое состояние игрок знает лучше всех (Client Side Prediction)
+ if (rId == clientId) return; // Свое состояние игрок знает лучше всех, (Client Side Prediction)
ClientState remoteState;
remoteState.id = rId;
@@ -230,6 +264,40 @@ namespace ZL {
remotePlayers[rId].add_state(remoteState);
}
}
+
+ if (msg.rfind("PLAYERINFO:", 0) == 0) {
+ if (parts.size() >= 4) {
+ try {
+ int pid = std::stoi(parts[1]);
+ if (pid == clientId) {
+ return;
+ }
+
+ std::string nick = parts[2];
+ int st = std::stoi(parts[3]);
+
+ auto it = remotePlayers.find(pid);
+ if (it != remotePlayers.end() && !it->second.timedStates.empty()) {
+ auto& states = it->second.timedStates;
+ states.back().nickname = nick;
+ states.back().shipType = st;
+ }
+ else {
+ ClientState cs;
+ cs.id = pid;
+ cs.nickname = nick;
+ cs.shipType = st;
+ cs.lastUpdateServerTime = std::chrono::system_clock::now();
+ remotePlayers[pid].add_state(cs);
+ }
+
+ std::cout << "Client: PLAYERINFO received. id=" << pid << " nick=" << nick << " shipType=" << st << std::endl;
+ }
+ catch (...) {
+ }
+ }
+ return;
+ }
}
std::string WebSocketClientBase::SignMessage(const std::string& msg) {
diff --git a/src/network/WebSocketClientBase.h b/src/network/WebSocketClientBase.h
index 1ca30cb..3784700 100644
--- a/src/network/WebSocketClientBase.h
+++ b/src/network/WebSocketClientBase.h
@@ -1,4 +1,4 @@
-#pragma once
+#pragma once
#include "NetworkInterface.h"
#include
@@ -13,8 +13,8 @@ namespace ZL {
protected:
std::unordered_map remotePlayers;
- // Серверные коробки
std::vector> serverBoxes_;
+ std::vector serverBoxesDestroyed_;
std::vector pendingProjectiles_;
std::vector pendingDeaths_;
@@ -40,6 +40,10 @@ namespace ZL {
return serverBoxes_;
}
+ std::vector getServerBoxDestroyedFlags() {
+ return serverBoxesDestroyed_;
+ }
+
std::vector getPendingProjectiles() override;
std::vector getPendingDeaths() override;
std::vector getPendingRespawns() override;
diff --git a/src/render/Renderer.cpp b/src/render/Renderer.cpp
index a11ed19..721ae54 100644
--- a/src/render/Renderer.cpp
+++ b/src/render/Renderer.cpp
@@ -895,41 +895,65 @@ namespace ZL {
static const std::string vColor("vColor");
static const std::string vTexCoord("vTexCoord");
static const std::string vPosition("vPosition");
-
- //glBindVertexArray(VertexRenderStruct.vao->getBuffer());
- //Check if main thread, check if data is not empty...
+ // On WebGL (and when not using VAO), vertex attribute arrays must be explicitly
+ // enabled before drawing. Desktop with VAO can rely on stored state; WebGL cannot.
if (VertexRenderStruct.data.NormalData.size() > 0)
{
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.normalVBO->getBuffer());
VertexAttribPointer3fv(vNormal, 0, NULL);
+ EnableVertexAttribArray(vNormal);
+ }
+ else
+ {
+ DisableVertexAttribArray(vNormal);
}
if (VertexRenderStruct.data.TangentData.size() > 0)
{
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.tangentVBO->getBuffer());
VertexAttribPointer3fv(vTangent, 0, NULL);
+ EnableVertexAttribArray(vTangent);
+ }
+ else
+ {
+ DisableVertexAttribArray(vTangent);
}
if (VertexRenderStruct.data.BinormalData.size() > 0)
{
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.binormalVBO->getBuffer());
VertexAttribPointer3fv(vBinormal, 0, NULL);
+ EnableVertexAttribArray(vBinormal);
+ }
+ else
+ {
+ DisableVertexAttribArray(vBinormal);
}
if (VertexRenderStruct.data.ColorData.size() > 0)
{
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.colorVBO->getBuffer());
VertexAttribPointer3fv(vColor, 0, NULL);
+ EnableVertexAttribArray(vColor);
+ }
+ else
+ {
+ DisableVertexAttribArray(vColor);
}
if (VertexRenderStruct.data.TexCoordData.size() > 0)
{
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.texCoordVBO->getBuffer());
VertexAttribPointer2fv(vTexCoord, 0, NULL);
+ EnableVertexAttribArray(vTexCoord);
+ }
+ else
+ {
+ DisableVertexAttribArray(vTexCoord);
}
glBindBuffer(GL_ARRAY_BUFFER, VertexRenderStruct.positionVBO->getBuffer());
VertexAttribPointer3fv(vPosition, 0, NULL);
+ EnableVertexAttribArray(vPosition);
glDrawArrays(GL_TRIANGLES, 0, static_cast(VertexRenderStruct.data.PositionData.size()));
-
}
void worldToScreenCoordinates(Vector3f objectPos,
diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp
index 585ca8d..73e2db1 100644
--- a/src/render/TextRenderer.cpp
+++ b/src/render/TextRenderer.cpp
@@ -362,9 +362,10 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
// 4. Рендеринг
r->shaderManager.PushShader(shaderName);
- // Матрица проекции (экрана)
- float W = (float)Environment::width;
- float H = (float)Environment::height;
+ // Матрица проекции — используем виртуальные проекционные размеры,
+ // чтобы координаты текста были независимы от физического разрешения экрана.
+ float W = Environment::projectionWidth;
+ float H = Environment::projectionHeight;
Eigen::Matrix4f proj = Eigen::Matrix4f::Identity();
proj(0, 0) = 2.0f / W;
proj(1, 1) = 2.0f / H;