Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,71 @@ jobs:
run: |
./scripts/generate-bindings.sh
git diff --exit-code -- crates/match-bindings

match-e2e:
runs-on: ubuntu-latest
timeout-minutes: 25

steps:
- uses: actions/checkout@v4

- name: Install pinned Rust toolchain
run: rustup show

- name: Cache Rust build outputs
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
workspaces: |
. -> target
modules/match -> modules/match/target

- name: Install pinned SpacetimeDB CLI
run: |
curl -sSf https://install.spacetimedb.com | sh -s -- --yes
export PATH="$HOME/.local/bin:$PATH"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
spacetime version install 2.7.1 --use --yes
./scripts/check-toolchain.sh

- name: Start local SpacetimeDB
run: |
spacetime start \
--listen-addr 127.0.0.1:3000 \
--data-dir "$GITHUB_WORKSPACE/.spacetime-e2e-data" \
--non-interactive \
> "$RUNNER_TEMP/spacetime-e2e.log" 2>&1 &
echo "$!" > "$RUNNER_TEMP/spacetime-e2e.pid"
for attempt in {1..60}; do
if curl --fail --silent --show-error http://127.0.0.1:3000/v1/metrics > /dev/null; then
exit 0
fi
sleep 1
done
cat "$RUNNER_TEMP/spacetime-e2e.log"
exit 1

- name: Publish fresh match database
run: |
spacetime publish \
--server local \
--module-path modules/match \
--delete-data=always \
--yes \
of-match-e2e-ci

- name: Run live match smoke
run: |
cargo run -p match-e2e -- \
--host http://127.0.0.1:3000 \
--database of-match-e2e-ci

- name: Stop local SpacetimeDB
if: always()
run: |
if [ -f "$RUNNER_TEMP/spacetime-e2e.pid" ]; then
kill "$(cat "$RUNNER_TEMP/spacetime-e2e.pid")" || true
fi
if [ -f "$RUNNER_TEMP/spacetime-e2e.log" ]; then
cat "$RUNNER_TEMP/spacetime-e2e.log"
fi
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ jobs:
- name: Build production deployment
run: ./scripts/build-vercel-production.sh

- name: Enforce web bundle size gate
run: ./scripts/measure-web-bundle.sh --dist target/web --enforce

- name: Publish authoritative module
env:
SPACETIMEDB_TOKEN: ${{ secrets.SPACETIMEDB_TOKEN }}
Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ Browser identity tokens are scoped by host, database, and profile in
`localStorage`. Production deployments must use HTTPS/WSS to protect those
credentials; WebGPU itself requires a secure context (localhost is allowed for
local development). Run `trunk build` from `crates/game-client` to
produce a deployable bundle in `target/web`.
produce a deployable bundle in `target/web`. Measure and enforce the download
budget with `./scripts/measure-web-bundle.sh --enforce` (see
[Browser release gates](docs/browser-gates.md)).

## Production deployment

Expand Down Expand Up @@ -263,6 +265,7 @@ relevant keys; `?` opens the field manual.
| `V` | Cycle map views |
| `?` | Toggle the field manual |
| `F3` | Toggle the performance overlay |
| `F4` | Toggle the observe event ring (`OF_OBSERVE=1` / `?observe=1` for console) |

A cluster is the full connected set of owned passable cells. Empty owned cells
can connect troop-bearing areas; blocked terrain and impassable elevation edges
Expand Down
1 change: 1 addition & 0 deletions crates/game-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ clap.workspace = true
bevy = { workspace = true, features = ["webgpu"] }
js-sys = "=0.3.103"
spacetimedb-sdk = { workspace = true, features = ["browser"] }
wasm-bindgen = "=0.2.126"
wasm-bindgen-futures = "=0.4.76"
web-sys = { version = "=0.3.103", features = [
"Location",
Expand Down
9 changes: 9 additions & 0 deletions crates/game-client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const DEFAULT_DATABASE: &str = match option_env!("OF_WEB_DATABASE") {
about = "Native hex RTS client",
disable_version_flag = true
)]
#[allow(clippy::struct_excessive_bools)]
struct ClientArgs {
/// Use the local deterministic fixture instead of `SpacetimeDB`.
#[arg(long)]
Expand Down Expand Up @@ -88,6 +89,10 @@ struct ClientArgs {
/// Skip the lobby UI and join immediately (env: `OF_AUTO_JOIN`).
#[arg(long)]
auto_join: bool,

/// Emit structured `[of.observe]` events to the log/console (env: `OF_OBSERVE`).
#[arg(long)]
observe: bool,
}

#[derive(Clone, Debug, Eq, PartialEq)]
Expand All @@ -109,6 +114,8 @@ pub struct ClientConfig {
pub profile: String,
/// Automation/dev path: connect and call `join_match` after bootstrap.
pub auto_join: bool,
/// Emit structured observe events to stderr / browser console.
pub observe: bool,
}

impl ClientConfig {
Expand Down Expand Up @@ -167,6 +174,7 @@ impl ClientConfig {
preferred_player,
profile,
auto_join,
observe: args.observe || env_flag("OF_OBSERVE"),
}
}

Expand Down Expand Up @@ -200,6 +208,7 @@ impl ClientConfig {
preferred_player,
profile,
auto_join,
observe: browser_flag("observe"),
}
}

Expand Down
112 changes: 111 additions & 1 deletion crates/game-client/src/hud.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bevy::{
app::AppExit,
picking::hover::Hovered,
prelude::*,
ui::UiRect,
Expand All @@ -10,7 +11,7 @@ use bevy::{
use crate::{
interaction::{InteractionState, OrderMode},
map_view::map_view_status_bundle,
model::{MatchView, ToastKind},
model::{MatchPhase, MatchView, ToastKind},
network::{ClientIntent, NetworkSet},
};

Expand Down Expand Up @@ -60,6 +61,13 @@ struct MobilizationSlider;
#[derive(Component)]
struct MobilizationThumb;

#[derive(Component)]
struct ResultOverlay;
#[derive(Component)]
struct ResultTitle;
#[derive(Component)]
struct ResultDetails;

#[derive(Component)]
struct CommandContextTitle;
#[derive(Component)]
Expand Down Expand Up @@ -130,6 +138,7 @@ impl Plugin for HudPlugin {
update_hud.after(NetworkSet::Apply),
update_command_bar.after(update_hud),
update_slider_visuals.after(update_hud),
leave_match_after_victory,
),
);
}
Expand All @@ -155,6 +164,7 @@ fn spawn_hud(mut commands: Commands, view: Res<MatchView>) {
spawn_bottom_bar(root, view.mobilization_target);
spawn_toast(root);
spawn_help(root);
spawn_result_overlay(root);
});
}

Expand Down Expand Up @@ -468,6 +478,51 @@ fn spawn_help(root: &mut ChildSpawnerCommands) {
});
}

fn spawn_result_overlay(root: &mut ChildSpawnerCommands) {
root.spawn((
Name::new("Match result overlay"),
ResultOverlay,
Node {
display: Display::None,
position_type: PositionType::Absolute,
left: percent(50),
top: percent(50),
width: px(480),
padding: UiRect::all(px(24)),
border: UiRect::all(px(2)),
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
row_gap: px(12),
..default()
},
UiTransform::from_translation(Val2::percent(-50.0, -50.0)),
GlobalZIndex(60),
BackgroundColor(Color::srgba(0.025, 0.039, 0.049, 0.985)),
BorderColor::all(CYAN),
Pickable::IGNORE,
))
.with_children(|overlay| {
overlay.spawn((
ResultTitle,
Text::new("MATCH COMPLETE"),
TextFont::from_font_size(24.0),
TextColor(CYAN),
Pickable::IGNORE,
));
overlay.spawn((
ResultDetails,
Text::new(""),
TextFont::from_font_size(13.0),
TextColor(TEXT),
Node {
align_self: AlignSelf::Stretch,
..default()
},
Pickable::IGNORE,
));
});
}

fn section_title(value: &'static str) -> impl Bundle {
(
Text::new(value),
Expand Down Expand Up @@ -682,10 +737,13 @@ fn update_hud(
Single<&mut Text, With<BottomStatus>>,
Single<&mut Text, With<MobilizationLabel>>,
Single<&mut Text, With<ToastText>>,
Single<&mut Text, With<ResultTitle>>,
Single<&mut Text, With<ResultDetails>>,
)>,
mut panels: ParamSet<(
Single<(&mut Node, &mut BackgroundColor, &mut BorderColor), With<ToastRoot>>,
Single<&mut Node, With<HelpPanel>>,
Single<(&mut Node, &mut BorderColor), With<ResultOverlay>>,
)>,
slider: Single<(Entity, &SliderValue), With<MobilizationSlider>>,
mut selection_totals: Local<SelectionTotalsCache>,
Expand Down Expand Up @@ -902,6 +960,58 @@ fn update_hud(
let mut toast_root = panels.p0();
toast_root.0.display = Display::None;
}

{
let mut overlay = panels.p2();
if let MatchPhase::Victory(winner) = view.phase {
let local_won = winner == u32::from(view.local_player);
overlay.0.display = Display::Flex;
overlay.1.set_all(if local_won { CYAN } else { CORAL });

let mut title = texts.p6();
set_text(
&mut title,
if local_won {
"VICTORY CONFIRMED".to_owned()
} else {
"MATCH COMPLETE".to_owned()
},
);

let mut details = texts.p7();
set_text(
&mut details,
format!(
"WINNER PLAYER {winner}\nLOCAL SEAT PLAYER {} // {}\nCONQUEST RESOLVED AT LOGICAL STEP {}\n\nESC // RETURN TO LOBBY DIRECTORY\nFROM THE DIRECTORY, SELECT LEAVE TO RETIRE THE LOBBY.",
view.local_player,
if local_won { "VICTORY" } else { "DEFEAT" },
view.logical_step,
),
);
} else {
overlay.0.display = Display::None;
}
}
}

fn leave_match_after_victory(
keyboard: Res<ButtonInput<KeyCode>>,
view: Res<MatchView>,
mut app_exit: MessageWriter<AppExit>,
) {
if !matches!(view.phase, MatchPhase::Victory(_)) || !keyboard.just_pressed(KeyCode::Escape) {
return;
}

#[cfg(target_arch = "wasm32")]
{
if let Some(window) = web_sys::window() {
let _ = window.location().set_href("/");
}
}

#[cfg(not(target_arch = "wasm32"))]
app_exit.write(AppExit::Success);
}

fn update_slider_visuals(
Expand Down
5 changes: 5 additions & 0 deletions crates/game-client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod lobby;
mod map_view;
mod model;
mod network;
mod observe;
mod online;
mod overlays;
mod performance;
Expand All @@ -35,6 +36,7 @@ use lobby::LobbyPlugin;
use map_view::MapViewPlugin;
use model::{MatchView, update_transient_state};
use network::{NetworkBoundaryPlugin, NetworkSet, OfflineTransportPlugin, apply_server_updates};
use observe::ObservePlugin;
use online::{OnlineSyncSet, OnlineTransportPlugin};
use overlays::OverlayPlugin;
use performance::PerformanceOverlayPlugin;
Expand Down Expand Up @@ -101,6 +103,9 @@ fn main() {
LobbyPlugin,
HudPlugin,
PerformanceOverlayPlugin,
ObservePlugin {
console_enabled: config.observe,
},
))
.insert_resource(ClearColor(Color::srgb(0.018, 0.025, 0.031)))
.insert_resource(GlobalAmbientLight {
Expand Down
Loading
Loading