Minor fixing

This commit is contained in:
Vladislav Khorev committed 2026-11-10 22:47:01 +06:00
1 parent 6cfd47e80d
commit 24c963c0a5
11 files changed
+123 -65

No files matched your search

+34 -3
View File
@@ -5,17 +5,28 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Shadow Over Bishkek</title> <title>Shadow Over Bishkek</title>
<style> <style>
:root {
/* Visible viewport size, kept in sync by syncViewport() below.
100vh on mobile is the *largest* viewport (URL bar hidden) and
would push the bottom of the canvas off-screen. */
--app-w: 100%;
--app-h: 100dvh;
}
body, html { body, html {
margin: 0; padding: 0; width: 100%; height: 100%; margin: 0; padding: 0; width: 100%; height: 100%;
overflow: hidden; background-color: #000; overflow: hidden; background-color: #000;
position: fixed; position: fixed; top: 0; left: 0;
} }
#canvas { #canvas {
display: block; display: block;
position: absolute; position: fixed;
top: 0; left: 0; top: 0; left: 0;
width: 100vw; height: 100vh; width: var(--app-w); height: var(--app-h);
border: none; border: none;
touch-action: none;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
} }
#status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } #status { color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
@@ -49,8 +60,28 @@
loadGameScript(); loadGameScript();
}); });
function syncViewport() {
var vv = window.visualViewport;
var w = (vv && vv.width) || window.innerWidth;
var h = (vv && vv.height) || window.innerHeight;
var root = document.documentElement;
root.style.setProperty('--app-w', Math.round(w) + 'px');
root.style.setProperty('--app-h', Math.round(h) + 'px');
}
syncViewport();
// Registered before the game script loads, so the CSS size is updated
// before the engine's own resize handler reads it.
window.addEventListener('resize', syncViewport);
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', function () {
syncViewport();
window.dispatchEvent(new Event('resize'));
});
}
window.addEventListener("orientationchange", function() { window.addEventListener("orientationchange", function() {
setTimeout(() => { setTimeout(() => {
syncViewport();
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
}, 200); }, 200);
}); });
+1 -2
View File
@@ -171,7 +171,6 @@ if(BUILD_TESTING)
target_include_directories( target_include_directories(
GameServerWebsocketLockstepTests PRIVATE "${BOOST_BUNDLED_DIR}") GameServerWebsocketLockstepTests PRIVATE "${BOOST_BUNDLED_DIR}")
target_compile_definitions( target_compile_definitions(
GameServerWebsocketLockstepTests PRIVATE NETWORK)
GameServerWebsocketLockstepTests PRIVATE GameServerWebsocketLockstepTests PRIVATE
NETWORK WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00) NETWORK WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0A00)
if(MSVC) if(MSVC)
@@ -197,7 +196,7 @@ if(BUILD_TESTING)
COMMAND GameServer --ticks 2 --no-sleep) COMMAND GameServer --ticks 2 --no-sleep)
add_test( add_test(
NAME GameServer.BattleZoneSharedPlayer NAME GameServer.BattleZoneSharedPlayer
COMMAND GameServer --campaign battle_zone --location uni_exterior COMMAND GameServer --campaign battle_zone --location park_korea
--ticks 2 --no-sleep) --ticks 2 --no-sleep)
set_tests_properties( set_tests_properties(
GameServer.BattleZoneSharedPlayer GameServer.BattleZoneSharedPlayer
+16 -11
View File
@@ -79,7 +79,7 @@ int main()
return fail("Battle Zone modes do not share their initial state"); return fail("Battle Zone modes do not share their initial state");
} }
if (multiplayerBattleZoneConfig->networkPlayerSlots.size() != 2 || if (multiplayerBattleZoneConfig->networkPlayerSlots.size() != 2 ||
multiplayerBattleZoneConfig->networkPlayerSlots[0] != "player" || multiplayerBattleZoneConfig->networkPlayerSlots[0] != "player1" ||
multiplayerBattleZoneConfig->networkPlayerSlots[1] != multiplayerBattleZoneConfig->networkPlayerSlots[1] !=
"player2" || "player2" ||
multiplayerBattleZoneConfig->networkCompatibility.empty()) { multiplayerBattleZoneConfig->networkCompatibility.empty()) {
@@ -140,7 +140,7 @@ int main()
disabledPlayer->enabled = false; disabledPlayer->enabled = false;
state.location->targetInteractiveObjectId = "object"; state.location->targetInteractiveObjectId = "object";
state.location->targetInteractNpcId = "npc"; state.location->targetInteractNpcId = "npc";
FRG::PlayerStateMap disabledPlayers{{"player", disabledPlayer}}; FRG::PlayerStateMap disabledPlayers{{"player1", disabledPlayer}};
const FRG::LocationSimulationEvents disabledEvents = const FRG::LocationSimulationEvents disabledEvents =
state.update(50, disabledPlayers, false); state.update(50, disabledPlayers, false);
if (!disabledEvents.interactiveObjects.empty() || if (!disabledEvents.interactiveObjects.empty() ||
@@ -165,7 +165,7 @@ int main()
playerTwo->enabled = true; playerTwo->enabled = true;
playerTwo->position = {2.f, 0.f, 0.f}; playerTwo->position = {2.f, 0.f, 0.f};
FRG::PlayerStateMap combatPlayers{ FRG::PlayerStateMap combatPlayers{
{"player", playerOne}, {"player2", playerTwo}}; {"player1", playerOne}, {"player2", playerTwo}};
state.update(100, combatPlayers, false); state.update(100, combatPlayers, false);
if (hostileNpc.attackTargetId != "player2") { if (hostileNpc.attackTargetId != "player2") {
@@ -181,7 +181,7 @@ int main()
playerTwo->hp = 0.f; playerTwo->hp = 0.f;
state.update(100, combatPlayers, false); state.update(100, combatPlayers, false);
if (hostileNpc.attackTargetId != "player") { if (hostileNpc.attackTargetId != "player1") {
return fail("Hostile NPC did not acquire a replacement after target death"); return fail("Hostile NPC did not acquire a replacement after target death");
} }
@@ -201,7 +201,7 @@ int main()
state.npcs["blocker"] = blocker; state.npcs["blocker"] = blocker;
hostileNpc.position = Eigen::Vector3f::Zero(); hostileNpc.position = Eigen::Vector3f::Zero();
hostileNpc.homePosition = {5.f, 0.f, 0.f}; hostileNpc.homePosition = {5.f, 0.f, 0.f};
hostileNpc.attackTargetId = "player"; hostileNpc.attackTargetId = "player1";
hostileNpc.requestedWalkTarget = playerOne->position; hostileNpc.requestedWalkTarget = playerOne->position;
hostileNpc.walkTarget = playerOne->position; hostileNpc.walkTarget = playerOne->position;
hostileNpc.pathWaypoints = {playerOne->position}; hostileNpc.pathWaypoints = {playerOne->position};
@@ -267,22 +267,22 @@ int main()
FRG::CampaignConfig multiplayerBattleZone = battleZone; FRG::CampaignConfig multiplayerBattleZone = battleZone;
multiplayerBattleZone.id = "battle_zone_multiplayer"; multiplayerBattleZone.id = "battle_zone_multiplayer";
multiplayerBattleZone.networkPlayerSlots = { multiplayerBattleZone.networkPlayerSlots = {
"player", "player2"}; "player1", "player2"};
networkServer.startCampaign(multiplayerBattleZone, {}, true, false); networkServer.startCampaign(multiplayerBattleZone, {}, true, false);
const auto networkSnapshot = networkServer.snapshot(); const auto networkSnapshot = networkServer.snapshot();
if (networkSnapshot->campaignId != "battle_zone_multiplayer") if (networkSnapshot->campaignId != "battle_zone_multiplayer")
return fail("Network campaign ID was not retained"); return fail("Network campaign ID was not retained");
if (networkSnapshot->players.count("player") != 1) if (networkSnapshot->players.count("player1") != 1)
return fail("Network campaign shared player was not created"); return fail("Network campaign shared player was not created");
if (networkSnapshot->players.count("player2") != 0) if (networkSnapshot->players.count("player2") != 0)
return fail("Network campaign unexpectedly created the test bot"); return fail("Network campaign unexpectedly created the test bot");
if (networkSnapshot->players.at("player").state.enabled) if (networkSnapshot->players.at("player1").state.enabled)
return fail("Network campaign player slot was enabled before connection"); return fail("Network campaign player slot was enabled before connection");
nlohmann::json loadedNetworkState; nlohmann::json loadedNetworkState;
networkServer.save(loadedNetworkState); networkServer.save(loadedNetworkState);
loadedNetworkState["players"]["player2"] = loadedNetworkState["players"]["player2"] =
loadedNetworkState["players"]["player"]; loadedNetworkState["players"]["player1"];
loadedNetworkState["players"]["player2"]["position"] = loadedNetworkState["players"]["player2"]["position"] =
{4.f, 0.f, 5.f}; {4.f, 0.f, 5.f};
networkServer.load(loadedNetworkState); networkServer.load(loadedNetworkState);
@@ -308,7 +308,7 @@ int main()
networkServer.submit(FRG::SetCharacterFacingAngleCommand{ networkServer.submit(FRG::SetCharacterFacingAngleCommand{
locationId, "player2", 1.5f, true}); locationId, "player2", 1.5f, true});
networkServer.submit(FRG::SetCharacterFaceTargetCommand{ networkServer.submit(FRG::SetCharacterFaceTargetCommand{
locationId, "player2", "player"}); locationId, "player2", "player1"});
networkServer.submit(FRG::SetCharacterEnabledCommand{ networkServer.submit(FRG::SetCharacterEnabledCommand{
locationId, "player2", false}); locationId, "player2", false});
const auto commandSnapshot = networkServer.snapshot(); const auto commandSnapshot = networkServer.snapshot();
@@ -371,6 +371,7 @@ int main()
moving->hp = 10.f; moving->hp = 10.f;
moving->pathWaypoints = {Eigen::Vector3f(8.f, 0.f, 0.f)}; moving->pathWaypoints = {Eigen::Vector3f(8.f, 0.f, 0.f)};
moving->movedLastTick = true; moving->movedLastTick = true;
moving->attackTargetId = "stationary";
FRG::PlayerStateMap occupancyPlayers{ FRG::PlayerStateMap occupancyPlayers{
{"stationary", stationary}, {"moving", moving}}; {"stationary", stationary}, {"moving", moving}};
if (occupancyState.collectPathObstacles( if (occupancyState.collectPathObstacles(
@@ -378,7 +379,11 @@ int main()
occupancyState.collectPathObstacles( occupancyState.collectPathObstacles(
"requester", occupancyPlayers, "moving").size() != 2 || "requester", occupancyPlayers, "moving").size() != 2 ||
!occupancyState.collectPathObstacles( !occupancyState.collectPathObstacles(
"stationary", occupancyPlayers).empty()) { "stationary", occupancyPlayers).empty() ||
!occupancyState.collectPathObstacles(
"moving", occupancyPlayers).empty() ||
occupancyState.collectPathObstacles(
"moving", occupancyPlayers, "stationary").size() != 1) {
return fail("Stationary obstacle collection policy is incorrect"); return fail("Stationary obstacle collection policy is incorrect");
} }
+13 -13
View File
@@ -23,12 +23,12 @@ int main()
const nlohmann::json original{ const nlohmann::json original{
{"schemaVersion", 2}, {"schemaVersion", 2},
{"campaignId", "battle_zone_multiplayer"}, {"campaignId", "battle_zone_multiplayer"},
{"players", {{"player", {{"hp", 200.0}}}}}, {"players", {{"player1", {{"hp", 200.0}}}}},
{"locations", {{"uni_exterior", {{"state", savedLocationState}}}}} {"locations", {{"park_korea", {{"state", savedLocationState}}}}}
}; };
FRG::InitialStateMessage initial; FRG::InitialStateMessage initial;
initial.state = original; initial.state = original;
initial.assignedPlayerId = "player"; initial.assignedPlayerId = "player1";
initial.snapshotTick = 42; initial.snapshotTick = 42;
initial.compatibilityFingerprint = "lockstep-v2:battle_zone_multiplayer"; initial.compatibilityFingerprint = "lockstep-v2:battle_zone_multiplayer";
const std::string message = FRG::makeInitialStateMessage(initial); const std::string message = FRG::makeInitialStateMessage(initial);
@@ -40,11 +40,11 @@ int main()
require(parsed.state == original, "state payload did not round trip"); require(parsed.state == original, "state payload did not round trip");
require(parsed.snapshotTick == 42, "snapshot tick did not round trip"); require(parsed.snapshotTick == 42, "snapshot tick did not round trip");
require( require(
parsed.assignedPlayerId == "player", parsed.assignedPlayerId == "player1",
"assigned player did not round trip"); "assigned player did not round trip");
require( require(
!parsed.state["locations"]["uni_exterior"]["state"].contains("cameraAzimuth") && !parsed.state["locations"]["park_korea"]["state"].contains("cameraAzimuth") &&
!parsed.state["locations"]["uni_exterior"]["state"].contains("cameraInclination"), !parsed.state["locations"]["park_korea"]["state"].contains("cameraInclination"),
"local camera state leaked into the network payload"); "local camera state leaked into the network payload");
FRG::DestinationInputMessage input; FRG::DestinationInputMessage input;
@@ -87,11 +87,11 @@ int main()
FRG::TickMessage tick; FRG::TickMessage tick;
tick.tickId = 43; tick.tickId = 43;
tick.commands.push_back(FRG::ScheduledDestinationCommand{ tick.commands.push_back(FRG::ScheduledDestinationCommand{
1, "player", parsedInput.target}); 1, "player1", parsedInput.target});
tick.commands.push_back(FRG::ScheduledCharacterEnabledCommand{ tick.commands.push_back(FRG::ScheduledCharacterEnabledCommand{
2, "uni_exterior", "player2", true}); 2, "park_korea", "player2", true});
tick.commands.push_back(FRG::ScheduledCharacterActionCommand{ tick.commands.push_back(FRG::ScheduledCharacterActionCommand{
3, "player", "set_target", "hostile_npc"}); 3, "player1", "set_target", "hostile_npc"});
tick.stateHash = "0123456789abcdef"; tick.stateHash = "0123456789abcdef";
FRG::TickMessage parsedTick; FRG::TickMessage parsedTick;
require( require(
@@ -147,9 +147,9 @@ int main()
FRG::LockstepTimeline timeline(2); FRG::LockstepTimeline timeline(2);
const std::uint64_t firstSequence = const std::uint64_t firstSequence =
timeline.schedule("player", Eigen::Vector3f(1.f, 0.f, 1.f)); timeline.schedule("player1", Eigen::Vector3f(1.f, 0.f, 1.f));
const std::uint64_t secondSequence = const std::uint64_t secondSequence =
timeline.schedule("player_2", Eigen::Vector3f(2.f, 0.f, 2.f)); timeline.schedule("player2", Eigen::Vector3f(2.f, 0.f, 2.f));
const FRG::TickMessage emptyTick = timeline.advance(); const FRG::TickMessage emptyTick = timeline.advance();
require( require(
emptyTick.tickId == 1 && emptyTick.commands.empty(), emptyTick.tickId == 1 && emptyTick.commands.empty(),
@@ -163,8 +163,8 @@ int main()
commandTick.commands[1]).commandSequence == secondSequence, commandTick.commands[1]).commandSequence == secondSequence,
"scheduled commands were not finalized in stable order"); "scheduled commands were not finalized in stable order");
timeline.scheduleCharacterEnabled("uni_exterior", "player2", true); timeline.scheduleCharacterEnabled("park_korea", "player2", true);
timeline.scheduleCharacterAction("player", "clear_attack_target"); timeline.scheduleCharacterAction("player1", "clear_attack_target");
const FRG::TickMessage presenceTick = timeline.advance(); const FRG::TickMessage presenceTick = timeline.advance();
require( require(
presenceTick.commands.size() == 1 && presenceTick.commands.size() == 1 &&
+19 -19
View File
@@ -59,13 +59,13 @@ int main()
FRG::CampaignConfig campaign; FRG::CampaignConfig campaign;
campaign.id = "test_lockstep"; campaign.id = "test_lockstep";
campaign.networkPlayerSlots = {"player", "player2"}; campaign.networkPlayerSlots = {"player1", "player2"};
FRG::InternalGameServer authoritative; FRG::InternalGameServer authoritative;
authoritative.startCampaign(campaign, {}, true, false); authoritative.startCampaign(campaign, {}, true, false);
nlohmann::json initialState; nlohmann::json initialState;
authoritative.save(initialState); authoritative.save(initialState);
initialState["players"]["player2"] = initialState["players"]["player"]; initialState["players"]["player2"] = initialState["players"]["player1"];
FRG::LocationGameState testLocation; FRG::LocationGameState testLocation;
auto hostileNpc = std::make_shared<FRG::CharacterState>(); auto hostileNpc = std::make_shared<FRG::CharacterState>();
hostileNpc->enabled = true; hostileNpc->enabled = true;
@@ -77,7 +77,7 @@ int main()
initialState["locations"]["test_location"] = std::move(testLocationState); initialState["locations"]["test_location"] = std::move(testLocationState);
authoritative.load(initialState); authoritative.load(initialState);
FRG::WebsocketGameServer server( FRG::WebsocketGameServer server(
authoritative, {"player", "player2"}, "lockstep-v2:test_lockstep", authoritative, {"player1", "player2"}, "lockstep-v2:test_lockstep",
"127.0.0.1", 0); "127.0.0.1", 0);
auto client = std::make_unique<FRG::WebsocketGameClient>( auto client = std::make_unique<FRG::WebsocketGameClient>(
"127.0.0.1", std::to_string(server.port()), "/"); "127.0.0.1", std::to_string(server.port()), "/");
@@ -91,7 +91,7 @@ int main()
return client->initialized(); return client->initialized();
}), }),
"client did not receive initial state"); "client did not receive initial state");
require(client->localPlayerId() == "player", "wrong player slot assigned"); require(client->localPlayerId() == "player1", "wrong player slot assigned");
auto secondClient = std::make_unique<FRG::WebsocketGameClient>( auto secondClient = std::make_unique<FRG::WebsocketGameClient>(
"127.0.0.1", std::to_string(server.port()), "/"); "127.0.0.1", std::to_string(server.port()), "/");
@@ -120,9 +120,9 @@ int main()
client->update(0); client->update(0);
secondClient->update(0); secondClient->update(0);
return client->snapshot()->players.at("player2").state.enabled && return client->snapshot()->players.at("player2").state.enabled &&
secondClient->snapshot()->players.at("player").state.enabled; secondClient->snapshot()->players.at("player1").state.enabled;
}) && }) &&
authoritative.snapshot()->players.at("player").state.enabled && authoritative.snapshot()->players.at("player1").state.enabled &&
authoritative.snapshot()->players.at("player2").state.enabled, authoritative.snapshot()->players.at("player2").state.enabled,
"connection presence was not broadcast to all replicas"); "connection presence was not broadcast to all replicas");
@@ -140,16 +140,16 @@ int main()
const auto first = client->snapshot(); const auto first = client->snapshot();
const auto second = secondClient->snapshot(); const auto second = secondClient->snapshot();
const auto host = authoritative.snapshot(); const auto host = authoritative.snapshot();
return first->players.at("player").state.requestedWalkTarget.isApprox( return first->players.at("player1").state.requestedWalkTarget.isApprox(
Eigen::Vector3f(1.23f, 0.f, 2.35f)) && Eigen::Vector3f(1.23f, 0.f, 2.35f)) &&
second->players.at("player2").state.requestedWalkTarget.isApprox( second->players.at("player2").state.requestedWalkTarget.isApprox(
Eigen::Vector3f(-1.23f, 0.f, 3.46f)) && Eigen::Vector3f(-1.23f, 0.f, 3.46f)) &&
first->players.at("player2").state.requestedWalkTarget.isApprox( first->players.at("player2").state.requestedWalkTarget.isApprox(
second->players.at("player2").state.requestedWalkTarget) && second->players.at("player2").state.requestedWalkTarget) &&
second->players.at("player").state.requestedWalkTarget.isApprox( second->players.at("player1").state.requestedWalkTarget.isApprox(
first->players.at("player").state.requestedWalkTarget) && first->players.at("player1").state.requestedWalkTarget) &&
host->players.at("player").state.requestedWalkTarget.isApprox( host->players.at("player1").state.requestedWalkTarget.isApprox(
first->players.at("player").state.requestedWalkTarget) && first->players.at("player1").state.requestedWalkTarget) &&
host->players.at("player2").state.requestedWalkTarget.isApprox( host->players.at("player2").state.requestedWalkTarget.isApprox(
second->players.at("player2").state.requestedWalkTarget); second->players.at("player2").state.requestedWalkTarget);
}); });
@@ -160,9 +160,9 @@ int main()
std::cerr << "destination revisions " << first->revision << ", " std::cerr << "destination revisions " << first->revision << ", "
<< second->revision << ", " << host->revision << "\n" << second->revision << ", " << host->revision << "\n"
<< "player targets " << "player targets "
<< first->players.at("player").state.requestedWalkTarget.transpose() << first->players.at("player1").state.requestedWalkTarget.transpose()
<< " / " << " / "
<< host->players.at("player").state.requestedWalkTarget.transpose() << host->players.at("player1").state.requestedWalkTarget.transpose()
<< "\nplayer2 targets " << "\nplayer2 targets "
<< second->players.at("player2").state.requestedWalkTarget.transpose() << second->players.at("player2").state.requestedWalkTarget.transpose()
<< " / " << " / "
@@ -183,11 +183,11 @@ int main()
waitFor([&]() { waitFor([&]() {
client->update(0); client->update(0);
secondClient->update(0); secondClient->update(0);
return client->snapshot()->players.at("player").state.attackTargetId == return client->snapshot()->players.at("player1").state.attackTargetId ==
"hostile_npc" && "hostile_npc" &&
secondClient->snapshot()->players.at("player").state.attackTargetId == secondClient->snapshot()->players.at("player1").state.attackTargetId ==
"hostile_npc" && "hostile_npc" &&
authoritative.snapshot()->players.at("player").state.attackTargetId == authoritative.snapshot()->players.at("player1").state.attackTargetId ==
"hostile_npc"; "hostile_npc";
}), }),
"target command was not delayed and rebroadcast"); "target command was not delayed and rebroadcast");
@@ -202,9 +202,9 @@ int main()
waitFor([&]() { waitFor([&]() {
client->update(0); client->update(0);
secondClient->update(0); secondClient->update(0);
return client->snapshot()->players.at("player").state.attackTargetId.empty() && return client->snapshot()->players.at("player1").state.attackTargetId.empty() &&
secondClient->snapshot()->players.at("player").state.attackTargetId.empty() && secondClient->snapshot()->players.at("player1").state.attackTargetId.empty() &&
authoritative.snapshot()->players.at("player").state.attackTargetId.empty(); authoritative.snapshot()->players.at("player1").state.attackTargetId.empty();
}), }),
"clear attack target command was not delayed and rebroadcast"); "clear attack target command was not delayed and rebroadcast");
+1 -1
View File
@@ -89,7 +89,7 @@ public:
bool showWeapon = true; bool showWeapon = true;
// --- Stable character references --- // --- Stable character references ---
// Empty means no target. "player" is the campaign player; NPC IDs are // Empty means no target. "player1" is the campaign player; NPC IDs are
// location-local and validated while loading location content. // location-local and validated while loading location content.
std::string attackTargetId; std::string attackTargetId;
std::string faceTargetId; std::string faceTargetId;
+4 -2
View File
@@ -558,8 +558,10 @@ namespace FRG
logger() << "[network] Campaign '" << campaignId logger() << "[network] Campaign '" << campaignId
<< "' selected; creating WebSocket client" << std::endl; << "' selected; creating WebSocket client" << std::endl;
// Use "ws://127.0.0.1:8080/" for a local development server. // Use "ws://127.0.0.1:8080/" for a local development server.
static const char* const kMultiplayerServerUrl =
"wss://api.shadowoverbishkek.com/server001/"; //static const char* const kMultiplayerServerUrl = "wss://api.shadowoverbishkek.com/server001/";
static const char* const kMultiplayerServerUrl = "ws://127.0.0.1:8080/";
WebsocketEndpoint endpoint; WebsocketEndpoint endpoint;
if (!endpoint.parse(kMultiplayerServerUrl)) { if (!endpoint.parse(kMultiplayerServerUrl)) {
logger() << "[network] Invalid multiplayer server URL: " logger() << "[network] Invalid multiplayer server URL: "
+5
View File
@@ -140,11 +140,16 @@ std::vector<PathFinder::DynamicObstacle> LocationGameState::collectPathObstacles
std::sort(characters.begin(), characters.end(), std::sort(characters.begin(), characters.end(),
[](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; }); [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; });
const CharacterState* requester = findCharacter(requesterId, players);
const std::string attackTargetId =
requester ? requester->attackTargetId : std::string();
std::vector<PathFinder::DynamicObstacle> obstacles; std::vector<PathFinder::DynamicObstacle> obstacles;
obstacles.reserve(characters.size()); obstacles.reserve(characters.size());
for (const auto& [id, character] : characters) { for (const auto& [id, character] : characters) {
if (id == requesterId || !character->enabled || character->hp <= 0.f) if (id == requesterId || !character->enabled || character->hp <= 0.f)
continue; continue;
if (id == attackTargetId && id != forcedObstacleId)
continue;
if (character->movedLastTick && id != forcedObstacleId) continue; if (character->movedLastTick && id != forcedObstacleId) continue;
obstacles.push_back({character->position, character->collisionRadius}); obstacles.push_back({character->position, character->collisionRadius});
} }
+1
View File
@@ -80,6 +80,7 @@ namespace FRG {
ScriptEngine::~ScriptEngine() = default; ScriptEngine::~ScriptEngine() = default;
void ScriptEngine::init(Location* loc, AudioPlayerAsync* audioPlayer, const std::string& scriptPath) { void ScriptEngine::init(Location* loc, AudioPlayerAsync* audioPlayer, const std::string& scriptPath) {
logger() << "ScriptEngine::init with script path: " << scriptPath << std::endl;
impl = std::make_unique<Impl>(); impl = std::make_unique<Impl>();
sol::state& lua = impl->lua; sol::state& lua = impl->lua;
+1 -1
View File
@@ -293,7 +293,7 @@ LocationDefinition LocationContentLoader::load(
for (const auto& item : root.value("npcs", json::array())) { for (const auto& item : root.value("npcs", json::array())) {
NpcData data; NpcData data;
data.id = item.at("id").get<std::string>(); data.id = item.at("id").get<std::string>();
if (data.id.empty() || data.id == "player" || if (data.id.empty() || data.id == "player1" ||
!ids.insert(data.id).second) { !ids.insert(data.id).second) {
throw std::runtime_error( throw std::runtime_error(
"Missing, duplicate, or reserved NPC id in " + "Missing, duplicate, or reserved NPC id in " +
+28 -13
View File
@@ -2,6 +2,7 @@
#include "Environment.h" #include "Environment.h"
#include "utils/Utils.h" #include "utils/Utils.h"
#include <iostream> #include <iostream>
#include <cmath>
#ifdef __ANDROID__ #ifdef __ANDROID__
#include <android/log.h> #include <android/log.h>
#endif #endif
@@ -75,14 +76,27 @@ extern "C" void EMSCRIPTEN_KEEPALIVE onFileSystemLoaded() {
} }
// Single definition of the "logical" viewport size: the *visible* area of the page
// (visualViewport), not 100vh, which on mobile includes the area under the URL bar.
static void getViewportCssSize(int& w, int& h) {
w = EM_ASM_INT({
var vv = window.visualViewport;
return Math.round((vv && vv.width) || window.innerWidth);
});
h = EM_ASM_INT({
var vv = window.visualViewport;
return Math.round((vv && vv.height) || window.innerHeight);
});
}
static void applyResize(int logicalW, int logicalH) { static void applyResize(int logicalW, int logicalH) {
// Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung) // Получаем коэффициент плотности пикселей (например, 2.625 на Pixel или 3.0 на Samsung)
double dpr = emscripten_get_device_pixel_ratio(); double dpr = emscripten_get_device_pixel_ratio();
//double dpr = 1; // low quality //double dpr = 1; // low quality
// Вычисляем реальные физические пиксели // Вычисляем реальные физические пиксели
int physicalW = static_cast<int>(logicalW * dpr); int physicalW = static_cast<int>(std::lround(logicalW * dpr));
int physicalH = static_cast<int>(logicalH * dpr); int physicalH = static_cast<int>(std::lround(logicalH * dpr));
// Устанавливаем размер внутреннего буфера канваса // Устанавливаем размер внутреннего буфера канваса
emscripten_set_canvas_element_size("#canvas", physicalW, physicalH); emscripten_set_canvas_element_size("#canvas", physicalW, physicalH);
@@ -94,6 +108,11 @@ static void applyResize(int logicalW, int logicalH) {
SDL_SetWindowSize(FRG::Environment::window, physicalW, physicalH); SDL_SetWindowSize(FRG::Environment::window, physicalW, physicalH);
} }
// SDL may have written an inline CSS size derived from the physical pixels, which
// overrides the stylesheet. Re-assert the CSS box so its aspect always matches the
// drawing buffer (otherwise the image is stretched / cropped on mobile).
emscripten_set_element_css_size("#canvas", logicalW, logicalH);
// Пушим событие, чтобы движок пересчитал матрицы проекции // Пушим событие, чтобы движок пересчитал матрицы проекции
SDL_Event e = {}; SDL_Event e = {};
e.type = SDL_WINDOWEVENT; e.type = SDL_WINDOWEVENT;
@@ -118,8 +137,8 @@ void MainLoop() {
// Теперь DOM перестроен, можно забирать актуальные размеры. // Теперь DOM перестроен, можно забирать актуальные размеры.
// Если canvas должен занимать все окно браузера: // Если canvas должен занимать все окно браузера:
int canvasW = EM_ASM_INT({ return window.innerWidth; }); int canvasW, canvasH;
int canvasH = EM_ASM_INT({ return window.innerHeight; }); getViewportCssSize(canvasW, canvasH);
applyResize(canvasW, canvasH); applyResize(canvasW, canvasH);
} }
@@ -141,14 +160,10 @@ EM_BOOL onWebGLContextRestored(int /*eventType*/, const void* /*reserved*/, void
EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) { EM_BOOL onWindowResized(int /*eventType*/, const EmscriptenUiEvent* e, void* /*userData*/) {
double w, h; int w, h;
emscripten_get_element_css_size("window", &w, &h); getViewportCssSize(w, h);
// Если "window" не поддерживается вашей версией SDK, используем данные из e: applyResize(w, h);
// int logicalW = e->windowInnerWidth;
// int logicalH = e->windowInnerHeight;
applyResize(static_cast<int>(w), static_cast<int>(h));
return EM_FALSE; return EM_FALSE;
} }
@@ -181,8 +196,8 @@ int main(int argc, char* argv[]) {
emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized); emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onWindowResized);
emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onFullscreenChanged); emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_TRUE, onFullscreenChanged);
int canvasW = EM_ASM_INT({ return window.innerWidth; }); int canvasW, canvasH;
int canvasH = EM_ASM_INT({ return window.innerHeight; }); getViewportCssSize(canvasW, canvasH);
applyResize(canvasW, canvasH); applyResize(canvasW, canvasH);
SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0"); SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");