chore: cargo fmt across workspace; add analytics domain to CSP
Build and Deploy / build-and-push (push) Successful in 4m46s
Build and Deploy / build-and-push (push) Successful in 4m46s
- Apply cargo fmt to solitaire_engine, solitaire_server formatting. - solitaire_server/src/lib.rs: add https://analytics.aleshym.co to script-src, img-src, and connect-src so the analytics beacon loads without a CSP violation. - docs and README updates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,11 @@ agentdb.rvf.lock
|
|||||||
# IDE project files
|
# IDE project files
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Browser e2e harness artifacts
|
||||||
|
solitaire_server/e2e/node_modules/
|
||||||
|
solitaire_server/e2e/playwright-report/
|
||||||
|
solitaire_server/e2e/test-results/
|
||||||
|
|
||||||
# Android signing keystores — never commit
|
# Android signing keystores — never commit
|
||||||
*.jks
|
*.jks
|
||||||
*.jks.bak
|
*.jks.bak
|
||||||
|
|||||||
@@ -118,8 +118,28 @@ cargo test -p solitaire_core -p solitaire_sync -p solitaire_data -p solitaire_se
|
|||||||
|
|
||||||
# Lint
|
# Lint
|
||||||
cargo clippy --workspace --all-targets -- -D warnings
|
cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
|
||||||
|
# Browser e2e smoke (starts solitaire_server automatically)
|
||||||
|
cd solitaire_server/e2e
|
||||||
|
npm ci
|
||||||
|
npx playwright install chromium
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Seed-batch cycle regression gate (thresholded)
|
||||||
|
npm run review:cycles:regression
|
||||||
|
|
||||||
|
# Loop-aware candidate benchmark (writes test-results/cycle-candidate.json)
|
||||||
|
npm run review:cycles:candidate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For layered engine-vs-UI automation design (Rust unit tests, wasm debug-API
|
||||||
|
integration tests, and Playwright UI validation), see
|
||||||
|
[docs/testing-architecture.md](docs/testing-architecture.md).
|
||||||
|
|
||||||
|
For Quaternions (`klondike` / `card_game`) dependency upgrades, use
|
||||||
|
[`scripts/update_quaternions_deps.sh`](scripts/update_quaternions_deps.sh) and
|
||||||
|
the runbook in [docs/card-game-integration.md](docs/card-game-integration.md).
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
Built on [Bevy](https://bevyengine.org/) and the wider Rust ecosystem
|
Built on [Bevy](https://bevyengine.org/) and the wider Rust ecosystem
|
||||||
|
|||||||
@@ -101,6 +101,11 @@ Our 767-line `solitaire_core::solver` reimplements the full game rules to run th
|
|||||||
### 5. JSON Serialisation / Persistence
|
### 5. JSON Serialisation / Persistence
|
||||||
`solitaire_core::GameState` serialises the full mid-game state to JSON via `serde` so the engine can save on exit and restore on launch. `KlondikeState` derives `Clone` + `Eq` + `Hash` but not `Serialize` / `Deserialize`. No upstream changes are needed — this is handled externally.
|
`solitaire_core::GameState` serialises the full mid-game state to JSON via `serde` so the engine can save on exit and restore on launch. `KlondikeState` derives `Clone` + `Eq` + `Hash` but not `Serialize` / `Deserialize`. No upstream changes are needed — this is handled externally.
|
||||||
|
|
||||||
|
**Current verification (2026-06-01):** `klondike v0.3.0` and `card_game v0.4.0`
|
||||||
|
crate manifests expose no `serde` dependency/feature, and source exports no
|
||||||
|
serde derives for instruction/state snapshot types. Keep Ferrous'
|
||||||
|
`SavedInstruction` bridge in place.
|
||||||
|
|
||||||
**Session history:** `StateSnapshot<G>` stores the pre-move game state and instruction. On load, the session is reconstructed from the serialised snapshot history — no full replay from seed needed.
|
**Session history:** `StateSnapshot<G>` stores the pre-move game state and instruction. On load, the session is reconstructed from the serialised snapshot history — no full replay from seed needed.
|
||||||
|
|
||||||
**In our wrapper:** Serialise the `solitaire_core` wrapper struct using newtypes. Define `SavedInstruction` (a `Serialize + Deserialize` mirror of `KlondikeInstruction`) and `SavedStateSnapshot`. Reconstruct `SessionState` from the deserialised history. Schema version field lives on our wrapper.
|
**In our wrapper:** Serialise the `solitaire_core` wrapper struct using newtypes. Define `SavedInstruction` (a `Serialize + Deserialize` mirror of `KlondikeInstruction`) and `SavedStateSnapshot`. Reconstruct `SessionState` from the deserialised history. Schema version field lives on our wrapper.
|
||||||
@@ -147,6 +152,29 @@ Steps in dependency order. Upstream issues #10, #11, and the solver are all merg
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Quaternions Upgrade Runbook
|
||||||
|
|
||||||
|
Use this sequence whenever upgrading `klondike` / `card_game` from the
|
||||||
|
Quaternions registry:
|
||||||
|
|
||||||
|
1. Review upstream changes/releases:
|
||||||
|
- <https://git.aleshym.co/Quaternions/card_game>
|
||||||
|
- <https://git.aleshym.co/Quaternions/klondike>
|
||||||
|
2. Run:
|
||||||
|
```bash
|
||||||
|
scripts/update_quaternions_deps.sh <klondike_version> <card_game_version>
|
||||||
|
```
|
||||||
|
3. If the script passes, inspect the resulting `Cargo.lock` diff and land the
|
||||||
|
upgrade with the normal PR flow.
|
||||||
|
|
||||||
|
The script enforces:
|
||||||
|
- lockfile update to requested versions
|
||||||
|
- `cargo test --workspace`
|
||||||
|
- `cargo clippy --workspace -- -D warnings`
|
||||||
|
- deterministic replay/debug-API smoke tests in `solitaire_wasm`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## What Does NOT Need to Change
|
## What Does NOT Need to Change
|
||||||
|
|
||||||
- The `solitaire_engine` Bevy layer — it works against `solitaire_core` types; changes are isolated to `solitaire_core`.
|
- The `solitaire_engine` Bevy layer — it works against `solitaire_core` types; changes are isolated to `solitaire_core`.
|
||||||
|
|||||||
@@ -52,3 +52,4 @@ web-sys = { version = "0.3", features = ["Storage", "Window"] }
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
solitaire_core = { workspace = true, features = ["test-support"] }
|
||||||
|
|||||||
@@ -373,9 +373,7 @@ fn play_on_draw(
|
|||||||
// When the stock pile is empty the draw action recycles the waste pile
|
// When the stock pile is empty the draw action recycles the waste pile
|
||||||
// back to stock. Play the flip sound at half volume to give audible
|
// back to stock. Play the flip sound at half volume to give audible
|
||||||
// feedback that distinguishes a recycle from a normal draw.
|
// feedback that distinguishes a recycle from a normal draw.
|
||||||
let stock_len = game
|
let stock_len = game.as_ref().map_or(1, |g| g.0.stock_cards().len()); // default > 0 → normal draw sound
|
||||||
.as_ref()
|
|
||||||
.map_or(1, |g| g.0.stock_cards().len()); // default > 0 → normal draw sound
|
|
||||||
|
|
||||||
if is_recycle(stock_len) {
|
if is_recycle(stock_len) {
|
||||||
let mut data = lib.flip.clone();
|
let mut data = lib.flip.clone();
|
||||||
|
|||||||
@@ -16,10 +16,9 @@ use bevy::color::Color;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::sprite::Anchor;
|
use bevy::sprite::Anchor;
|
||||||
use bevy::window::WindowResized;
|
use bevy::window::WindowResized;
|
||||||
|
use klondike::{Foundation, KlondikePile, Tableau};
|
||||||
use solitaire_core::card::{Card, Rank, Suit};
|
use solitaire_core::card::{Card, Rank, Suit};
|
||||||
use solitaire_core::game_state::{DrawMode, GameState};
|
use solitaire_core::game_state::{DrawMode, GameState};
|
||||||
use klondike::{Foundation, KlondikePile, Tableau};
|
|
||||||
|
|
||||||
|
|
||||||
use crate::animation_plugin::{CARD_ANIM_Z_LIFT, CardAnim, EffectiveSlideDuration};
|
use crate::animation_plugin::{CARD_ANIM_Z_LIFT, CardAnim, EffectiveSlideDuration};
|
||||||
use crate::card_animation::CardAnimation;
|
use crate::card_animation::CardAnimation;
|
||||||
@@ -2575,8 +2574,7 @@ mod tests {
|
|||||||
"need at least 3 waste cards for this test"
|
"need at least 3 waste cards for this test"
|
||||||
);
|
);
|
||||||
|
|
||||||
let waste_ids: std::collections::HashSet<u32> =
|
let waste_ids: std::collections::HashSet<u32> = waste_pile.iter().map(|c| c.id).collect();
|
||||||
waste_pile.iter().map(|c| c.id).collect();
|
|
||||||
|
|
||||||
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
@@ -2628,8 +2626,7 @@ mod tests {
|
|||||||
let count = waste_pile.len();
|
let count = waste_pile.len();
|
||||||
assert!(count >= 2, "need at least 2 waste cards");
|
assert!(count >= 2, "need at least 2 waste cards");
|
||||||
|
|
||||||
let waste_ids: std::collections::HashSet<u32> =
|
let waste_ids: std::collections::HashSet<u32> = waste_pile.iter().map(|c| c.id).collect();
|
||||||
waste_pile.iter().map(|c| c.id).collect();
|
|
||||||
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
let layout = crate::layout::compute_layout(Vec2::new(1280.0, 800.0), 0.0, 0.0, true);
|
||||||
let positions = card_positions(&g, &layout);
|
let positions = card_positions(&g, &layout);
|
||||||
|
|
||||||
|
|||||||
@@ -331,7 +331,11 @@ fn update_drop_target_overlays(
|
|||||||
/// for everything else it is card-sized. Replicated here rather than
|
/// for everything else it is card-sized. Replicated here rather than
|
||||||
/// imported because `pile_drop_rect` is private to `input_plugin` and
|
/// imported because `pile_drop_rect` is private to `input_plugin` and
|
||||||
/// this overlay is the only other consumer.
|
/// this overlay is the only other consumer.
|
||||||
fn drop_overlay_rect(pile: &KlondikePile, layout: &Layout, game: &GameState) -> Option<(Vec2, Vec2)> {
|
fn drop_overlay_rect(
|
||||||
|
pile: &KlondikePile,
|
||||||
|
layout: &Layout,
|
||||||
|
game: &GameState,
|
||||||
|
) -> Option<(Vec2, Vec2)> {
|
||||||
let centre = layout.pile_positions.get(pile).copied()?;
|
let centre = layout.pile_positions.get(pile).copied()?;
|
||||||
if matches!(pile, KlondikePile::Tableau(_)) {
|
if matches!(pile, KlondikePile::Tableau(_)) {
|
||||||
let card_count = game.pile(*pile).len();
|
let card_count = game.pile(*pile).len();
|
||||||
@@ -658,5 +662,4 @@ mod tests {
|
|||||||
"Tableau(2) must not be highlighted for an illegal drop, got {overlays:?}"
|
"Tableau(2) must not be highlighted for an illegal drop, got {overlays:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
|
|
||||||
use bevy::input::ButtonInput;
|
use bevy::input::ButtonInput;
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use chrono::{DateTime, Duration, Local, NaiveDate, Utc};
|
|
||||||
use solitaire_data::{daily_seed_for, save_progress_to};
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||||
|
use chrono::{DateTime, Duration, Local, NaiveDate, Utc};
|
||||||
|
use solitaire_data::{daily_seed_for, save_progress_to};
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use solitaire_sync::ChallengeGoal;
|
use solitaire_sync::ChallengeGoal;
|
||||||
|
|
||||||
@@ -354,7 +354,6 @@ fn check_date_rollover(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
@@ -849,8 +849,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn shake_anim_skipped_under_reduce_motion() {
|
fn shake_anim_skipped_under_reduce_motion() {
|
||||||
use bevy::ecs::message::Messages;
|
use bevy::ecs::message::Messages;
|
||||||
use solitaire_core::game_state::{DrawMode, GameState};
|
|
||||||
use klondike::Tableau;
|
use klondike::Tableau;
|
||||||
|
use solitaire_core::game_state::{DrawMode, GameState};
|
||||||
use solitaire_data::Settings;
|
use solitaire_data::Settings;
|
||||||
|
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ use chrono::Utc;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||||
use bevy::window::AppLifecycle;
|
use bevy::window::AppLifecycle;
|
||||||
use solitaire_core::game_state::{DrawMode, GameMode, GameState};
|
|
||||||
use klondike::KlondikePile;
|
use klondike::KlondikePile;
|
||||||
|
use solitaire_core::game_state::{DrawMode, GameMode, GameState};
|
||||||
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve};
|
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve};
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
use solitaire_data::latest_replay_path;
|
use solitaire_data::latest_replay_path;
|
||||||
@@ -521,10 +521,7 @@ fn handle_new_game(
|
|||||||
// hides that information and reads naturally as "dealt from the
|
// hides that information and reads naturally as "dealt from the
|
||||||
// deck." Skipped when LayoutResource isn't present (headless tests).
|
// deck." Skipped when LayoutResource isn't present (headless tests).
|
||||||
if let Some(layout) = layout.as_ref()
|
if let Some(layout) = layout.as_ref()
|
||||||
&& let Some(stock) = layout
|
&& let Some(stock) = layout.0.pile_positions.get(&klondike::KlondikePile::Stock)
|
||||||
.0
|
|
||||||
.pile_positions
|
|
||||||
.get(&klondike::KlondikePile::Stock)
|
|
||||||
{
|
{
|
||||||
for mut tx in &mut card_transforms {
|
for mut tx in &mut card_transforms {
|
||||||
tx.translation.x = stock.x;
|
tx.translation.x = stock.x;
|
||||||
@@ -1047,17 +1044,11 @@ fn foundation_slot(foundation: klondike::Foundation) -> Option<u8> {
|
|||||||
/// previous heuristic incorrectly did (Quat hit this with 4 cards
|
/// previous heuristic incorrectly did (Quat hit this with 4 cards
|
||||||
/// remaining and the game just sat there).
|
/// remaining and the game just sat there).
|
||||||
pub fn has_legal_moves(game: &GameState) -> bool {
|
pub fn has_legal_moves(game: &GameState) -> bool {
|
||||||
|
|
||||||
|
|
||||||
// Drawing from a non-empty stock, and recycling a non-empty waste back to
|
// Drawing from a non-empty stock, and recycling a non-empty waste back to
|
||||||
// stock, are always legal moves in standard Klondike (unlimited recycles).
|
// stock, are always legal moves in standard Klondike (unlimited recycles).
|
||||||
// A game can only be genuinely stuck when both stock AND waste are exhausted.
|
// A game can only be genuinely stuck when both stock AND waste are exhausted.
|
||||||
let stock_empty = game
|
let stock_empty = game.stock_cards().is_empty();
|
||||||
.stock_cards()
|
let waste_empty = game.waste_cards().is_empty();
|
||||||
.is_empty();
|
|
||||||
let waste_empty = game
|
|
||||||
.waste_cards()
|
|
||||||
.is_empty();
|
|
||||||
if !stock_empty || !waste_empty {
|
if !stock_empty || !waste_empty {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1191,7 +1182,10 @@ fn handle_game_over_input(
|
|||||||
|
|
||||||
if keys.just_pressed(KeyCode::KeyN) || keys.just_pressed(KeyCode::Escape) {
|
if keys.just_pressed(KeyCode::KeyN) || keys.just_pressed(KeyCode::Escape) {
|
||||||
// confirmed: true — the game is already stuck; no abandon-confirmation needed.
|
// confirmed: true — the game is already stuck; no abandon-confirmation needed.
|
||||||
new_game.write(NewGameRequestEvent { confirmed: true, ..default() });
|
new_game.write(NewGameRequestEvent {
|
||||||
|
confirmed: true,
|
||||||
|
..default()
|
||||||
|
});
|
||||||
} else if keys.just_pressed(KeyCode::KeyU) {
|
} else if keys.just_pressed(KeyCode::KeyU) {
|
||||||
for entity in &screens {
|
for entity in &screens {
|
||||||
commands.entity(entity).despawn();
|
commands.entity(entity).despawn();
|
||||||
@@ -1219,7 +1213,10 @@ fn handle_game_over_button_input(
|
|||||||
}
|
}
|
||||||
if new_game_buttons.iter().any(|i| *i == Interaction::Pressed) {
|
if new_game_buttons.iter().any(|i| *i == Interaction::Pressed) {
|
||||||
// confirmed: true — the game is already stuck; no abandon-confirmation needed.
|
// confirmed: true — the game is already stuck; no abandon-confirmation needed.
|
||||||
new_game.write(NewGameRequestEvent { confirmed: true, ..default() });
|
new_game.write(NewGameRequestEvent {
|
||||||
|
confirmed: true,
|
||||||
|
..default()
|
||||||
|
});
|
||||||
} else if undo_buttons.iter().any(|i| *i == Interaction::Pressed) {
|
} else if undo_buttons.iter().any(|i| *i == Interaction::Pressed) {
|
||||||
for entity in &screens {
|
for entity in &screens {
|
||||||
commands.entity(entity).despawn();
|
commands.entity(entity).despawn();
|
||||||
@@ -1388,9 +1385,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn new_game_request_reseeds() {
|
fn new_game_request_reseeds() {
|
||||||
let mut app = test_app(1);
|
let mut app = test_app(1);
|
||||||
let before: Vec<u32> = app.world().resource::<GameStateResource>().0.pile(KlondikePile::Tableau(
|
let before: Vec<u32> = app
|
||||||
Tableau::Tableau1,
|
.world()
|
||||||
))
|
.resource::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.pile(KlondikePile::Tableau(Tableau::Tableau1))
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.id)
|
.map(|c| c.id)
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1402,9 +1401,11 @@ mod tests {
|
|||||||
});
|
});
|
||||||
app.update();
|
app.update();
|
||||||
|
|
||||||
let after: Vec<u32> = app.world().resource::<GameStateResource>().0.pile(KlondikePile::Tableau(
|
let after: Vec<u32> = app
|
||||||
Tableau::Tableau1,
|
.world()
|
||||||
))
|
.resource::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.pile(KlondikePile::Tableau(Tableau::Tableau1))
|
||||||
.iter()
|
.iter()
|
||||||
.map(|c| c.id)
|
.map(|c| c.id)
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1415,17 +1416,25 @@ mod tests {
|
|||||||
fn settings_changed_updates_take_from_foundation_flag() {
|
fn settings_changed_updates_take_from_foundation_flag() {
|
||||||
let mut app = test_app(1);
|
let mut app = test_app(1);
|
||||||
assert!(
|
assert!(
|
||||||
app.world().resource::<GameStateResource>().0.take_from_foundation,
|
app.world()
|
||||||
|
.resource::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.take_from_foundation,
|
||||||
"fresh game should inherit default take_from_foundation=true",
|
"fresh game should inherit default take_from_foundation=true",
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut settings = solitaire_data::Settings::default();
|
let mut settings = solitaire_data::Settings::default();
|
||||||
settings.take_from_foundation = false;
|
settings.take_from_foundation = false;
|
||||||
app.world_mut()
|
app.world_mut()
|
||||||
.write_message(crate::settings_plugin::SettingsChangedEvent(settings.clone()));
|
.write_message(crate::settings_plugin::SettingsChangedEvent(
|
||||||
|
settings.clone(),
|
||||||
|
));
|
||||||
app.update();
|
app.update();
|
||||||
assert!(
|
assert!(
|
||||||
!app.world().resource::<GameStateResource>().0.take_from_foundation,
|
!app.world()
|
||||||
|
.resource::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.take_from_foundation,
|
||||||
"settings event must forward take_from_foundation=false into live game state",
|
"settings event must forward take_from_foundation=false into live game state",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1434,7 +1443,10 @@ mod tests {
|
|||||||
.write_message(crate::settings_plugin::SettingsChangedEvent(settings));
|
.write_message(crate::settings_plugin::SettingsChangedEvent(settings));
|
||||||
app.update();
|
app.update();
|
||||||
assert!(
|
assert!(
|
||||||
app.world().resource::<GameStateResource>().0.take_from_foundation,
|
app.world()
|
||||||
|
.resource::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.take_from_foundation,
|
||||||
"settings event must forward take_from_foundation=true into live game state",
|
"settings event must forward take_from_foundation=true into live game state",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1634,7 +1646,9 @@ mod tests {
|
|||||||
// Build a tableau with two face-up cards.
|
// Build a tableau with two face-up cards.
|
||||||
{
|
{
|
||||||
let mut gs = app.world_mut().resource_mut::<GameStateResource>();
|
let mut gs = app.world_mut().resource_mut::<GameStateResource>();
|
||||||
gs.0.set_test_tableau_cards(Tableau::Tableau1, vec![
|
gs.0.set_test_tableau_cards(
|
||||||
|
Tableau::Tableau1,
|
||||||
|
vec![
|
||||||
Card {
|
Card {
|
||||||
id: 910,
|
id: 910,
|
||||||
suit: Suit::Clubs,
|
suit: Suit::Clubs,
|
||||||
@@ -1647,7 +1661,8 @@ mod tests {
|
|||||||
rank: Rank::Queen,
|
rank: Rank::Queen,
|
||||||
face_up: true,
|
face_up: true,
|
||||||
},
|
},
|
||||||
]);
|
],
|
||||||
|
);
|
||||||
gs.0.set_test_tableau_cards(
|
gs.0.set_test_tableau_cards(
|
||||||
Tableau::Tableau2,
|
Tableau::Tableau2,
|
||||||
vec![Card {
|
vec![Card {
|
||||||
@@ -2359,7 +2374,10 @@ mod tests {
|
|||||||
Tableau::Tableau7,
|
Tableau::Tableau7,
|
||||||
] {
|
] {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
app.world().resource::<GameStateResource>().0.pile(KlondikePile::Tableau(tableau)),
|
app.world()
|
||||||
|
.resource::<GameStateResource>()
|
||||||
|
.0
|
||||||
|
.pile(KlondikePile::Tableau(tableau)),
|
||||||
expected.pile(KlondikePile::Tableau(tableau)),
|
expected.pile(KlondikePile::Tableau(tableau)),
|
||||||
"tableau column {tableau:?} must match the unfiltered seed",
|
"tableau column {tableau:?} must match the unfiltered seed",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2404,7 +2404,10 @@ fn update_selection_hud(
|
|||||||
/// When the slot has a claimed suit (any card has landed) the announcement is
|
/// When the slot has a claimed suit (any card has landed) the announcement is
|
||||||
/// "▶ {Suit} Foundation"; while the slot is empty it falls back to a
|
/// "▶ {Suit} Foundation"; while the slot is empty it falls back to a
|
||||||
/// "▶ Foundation N" placeholder labelled by the 1-based slot index.
|
/// "▶ Foundation N" placeholder labelled by the 1-based slot index.
|
||||||
fn foundation_selection_label(slot: Foundation, game: &solitaire_core::game_state::GameState) -> String {
|
fn foundation_selection_label(
|
||||||
|
slot: Foundation,
|
||||||
|
game: &solitaire_core::game_state::GameState,
|
||||||
|
) -> String {
|
||||||
let claimed = game
|
let claimed = game
|
||||||
.pile(KlondikePile::Foundation(slot))
|
.pile(KlondikePile::Foundation(slot))
|
||||||
.first()
|
.first()
|
||||||
|
|||||||
@@ -269,7 +269,10 @@ pub fn compute_layout(
|
|||||||
5 => Tableau::Tableau6,
|
5 => Tableau::Tableau6,
|
||||||
_ => Tableau::Tableau7,
|
_ => Tableau::Tableau7,
|
||||||
};
|
};
|
||||||
pile_positions.insert(KlondikePile::Tableau(tableau), Vec2::new(col_x(i), tableau_y));
|
pile_positions.insert(
|
||||||
|
KlondikePile::Tableau(tableau),
|
||||||
|
Vec2::new(col_x(i), tableau_y),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adaptive tableau fan fraction. On height-limited (desktop) windows the
|
// Adaptive tableau fan fraction. On height-limited (desktop) windows the
|
||||||
@@ -339,7 +342,9 @@ mod tests {
|
|||||||
Tableau::Tableau7,
|
Tableau::Tableau7,
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
layout.pile_positions.contains_key(&KlondikePile::Tableau(tableau)),
|
layout
|
||||||
|
.pile_positions
|
||||||
|
.contains_key(&KlondikePile::Tableau(tableau)),
|
||||||
"missing tableau {tableau:?}"
|
"missing tableau {tableau:?}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -758,7 +763,11 @@ mod tests {
|
|||||||
let window = Vec2::new(360.0, 800.0);
|
let window = Vec2::new(360.0, 800.0);
|
||||||
let without = compute_layout(window, 0.0, 0.0, true);
|
let without = compute_layout(window, 0.0, 0.0, true);
|
||||||
let with_inset = compute_layout(window, 0.0, 48.0, true);
|
let with_inset = compute_layout(window, 0.0, 48.0, true);
|
||||||
for pile in [KlondikePile::Stock, KlondikePile::Tableau(Tableau::Tableau1), KlondikePile::Tableau(Tableau::Tableau7)] {
|
for pile in [
|
||||||
|
KlondikePile::Stock,
|
||||||
|
KlondikePile::Tableau(Tableau::Tableau1),
|
||||||
|
KlondikePile::Tableau(Tableau::Tableau7),
|
||||||
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
(without.pile_positions[&pile].x - with_inset.pile_positions[&pile].x).abs() < 1e-3,
|
(without.pile_positions[&pile].x - with_inset.pile_positions[&pile].x).abs() < 1e-3,
|
||||||
"{pile:?} x-position must not change with safe_area_bottom",
|
"{pile:?} x-position must not change with safe_area_bottom",
|
||||||
|
|||||||
@@ -153,7 +153,6 @@ pub use safe_area::{SafeAreaAnchoredTop, SafeAreaInsets, SafeAreaInsetsPlugin};
|
|||||||
pub use selection_plugin::{
|
pub use selection_plugin::{
|
||||||
KeyboardDragState, SelectionHighlight, SelectionPlugin, SelectionState,
|
KeyboardDragState, SelectionHighlight, SelectionPlugin, SelectionState,
|
||||||
};
|
};
|
||||||
pub use touch_selection_plugin::{TouchSelectionPlugin, TouchSelectionState};
|
|
||||||
pub use settings_plugin::{
|
pub use settings_plugin::{
|
||||||
PendingWindowGeometry, SFX_STEP, SettingsChangedEvent, SettingsPlugin, SettingsResource,
|
PendingWindowGeometry, SFX_STEP, SettingsChangedEvent, SettingsPlugin, SettingsResource,
|
||||||
SettingsScreen, WINDOW_GEOMETRY_DEBOUNCE_SECS,
|
SettingsScreen, WINDOW_GEOMETRY_DEBOUNCE_SECS,
|
||||||
@@ -179,6 +178,7 @@ pub use theme::{
|
|||||||
pub use time_attack_plugin::{
|
pub use time_attack_plugin::{
|
||||||
TIME_ATTACK_DURATION_SECS, TimeAttackEndedEvent, TimeAttackPlugin, TimeAttackResource,
|
TIME_ATTACK_DURATION_SECS, TimeAttackEndedEvent, TimeAttackPlugin, TimeAttackResource,
|
||||||
};
|
};
|
||||||
|
pub use touch_selection_plugin::{TouchSelectionPlugin, TouchSelectionState};
|
||||||
pub use ui_focus::{Disabled, FocusGroup, Focusable, FocusedButton, UiFocusPlugin};
|
pub use ui_focus::{Disabled, FocusGroup, Focusable, FocusedButton, UiFocusPlugin};
|
||||||
pub use ui_modal::{
|
pub use ui_modal::{
|
||||||
ButtonVariant, ModalActions, ModalBody, ModalButton, ModalCard, ModalHeader, ModalScrim,
|
ButtonVariant, ModalActions, ModalBody, ModalButton, ModalCard, ModalHeader, ModalScrim,
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
use bevy::tasks::{AsyncComputeTaskPool, Task, futures_lite::future};
|
||||||
use solitaire_core::game_state::GameState;
|
|
||||||
use klondike::KlondikePile;
|
use klondike::KlondikePile;
|
||||||
|
use solitaire_core::game_state::GameState;
|
||||||
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve_from_state};
|
use solitaire_core::solver::{SolverConfig, SolverResult, try_solve_from_state};
|
||||||
|
|
||||||
use crate::card_plugin::CardEntity;
|
use crate::card_plugin::CardEntity;
|
||||||
@@ -101,7 +101,10 @@ struct HintTask {
|
|||||||
enum HintTaskOutput {
|
enum HintTaskOutput {
|
||||||
/// Solver verdict was `Winnable`; here is the first move on the
|
/// Solver verdict was `Winnable`; here is the first move on the
|
||||||
/// solution path.
|
/// solution path.
|
||||||
SolverMove { from: KlondikePile, to: KlondikePile },
|
SolverMove {
|
||||||
|
from: KlondikePile,
|
||||||
|
to: KlondikePile,
|
||||||
|
},
|
||||||
/// Solver was `Unwinnable` or `Inconclusive`. The poll system
|
/// Solver was `Unwinnable` or `Inconclusive`. The poll system
|
||||||
/// runs the legacy heuristic against the live `GameState` so the
|
/// runs the legacy heuristic against the live `GameState` so the
|
||||||
/// H key always produces feedback while any legal move exists.
|
/// H key always produces feedback while any legal move exists.
|
||||||
|
|||||||
@@ -97,7 +97,11 @@ pub(crate) fn format_move_body(m: &ReplayMove) -> String {
|
|||||||
match m {
|
match m {
|
||||||
ReplayMove::StockClick => "stock cycle".to_string(),
|
ReplayMove::StockClick => "stock cycle".to_string(),
|
||||||
ReplayMove::Move { from, to, .. } => {
|
ReplayMove::Move { from, to, .. } => {
|
||||||
format!("{} \u{2192} {}", format_saved_pile(from), format_saved_pile(to))
|
format!(
|
||||||
|
"{} \u{2192} {}",
|
||||||
|
format_saved_pile(from),
|
||||||
|
format_saved_pile(to)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,15 +25,14 @@
|
|||||||
|
|
||||||
mod format;
|
mod format;
|
||||||
mod input;
|
mod input;
|
||||||
mod update;
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
mod update;
|
||||||
|
|
||||||
pub(crate) use self::format::*;
|
pub(crate) use self::format::*;
|
||||||
pub(crate) use self::input::*;
|
pub(crate) use self::input::*;
|
||||||
pub(crate) use self::update::*;
|
pub(crate) use self::update::*;
|
||||||
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use crate::events::{DrawRequestEvent, MoveRequestEvent, StateChangedEvent, UndoRequestEvent};
|
use crate::events::{DrawRequestEvent, MoveRequestEvent, StateChangedEvent, UndoRequestEvent};
|
||||||
use crate::font_plugin::FontResource;
|
use crate::font_plugin::FontResource;
|
||||||
use crate::platform::SHOW_KEYBOARD_ACCELERATORS;
|
use crate::platform::SHOW_KEYBOARD_ACCELERATORS;
|
||||||
@@ -44,6 +43,7 @@ use crate::ui_theme::{
|
|||||||
STATE_SUCCESS, STATE_SUCCESS_HC, TEXT_PRIMARY, TEXT_PRIMARY_HC, TEXT_SECONDARY, TYPE_BODY,
|
STATE_SUCCESS, STATE_SUCCESS_HC, TEXT_PRIMARY, TEXT_PRIMARY_HC, TEXT_SECONDARY, TYPE_BODY,
|
||||||
TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_4, Z_DROP_OVERLAY,
|
TYPE_CAPTION, TYPE_HEADLINE, VAL_SPACE_1, VAL_SPACE_2, VAL_SPACE_4, Z_DROP_OVERLAY,
|
||||||
};
|
};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Z-index — see `ui_theme::Z_MODAL_SCRIM` (200) for the next layer above.
|
// Z-index — see `ui_theme::Z_MODAL_SCRIM` (200) for the next layer above.
|
||||||
@@ -316,7 +316,6 @@ pub struct ReplayOverlayScrubNotch;
|
|||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct ReplayOverlayScrubNotchLabel;
|
pub struct ReplayOverlayScrubNotchLabel;
|
||||||
|
|
||||||
|
|
||||||
/// Marker on the keybind-hint footer row at the bottom edge of the
|
/// Marker on the keybind-hint footer row at the bottom edge of the
|
||||||
/// banner. Carries two `Text` children: a vim-style mode indicator
|
/// banner. Carries two `Text` children: a vim-style mode indicator
|
||||||
/// (`▌ NORMAL │ replay`) on the left and the keybind hint
|
/// (`▌ NORMAL │ replay`) on the left and the keybind hint
|
||||||
@@ -1270,4 +1269,3 @@ fn win_move_marker_pct(state: &ReplayPlaybackState) -> Option<f32> {
|
|||||||
let frac = (idx as f32 / total as f32).clamp(0.0, 1.0);
|
let frac = (idx as f32 / total as f32).clamp(0.0, 1.0);
|
||||||
Some(frac * 100.0)
|
Some(frac * 100.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -854,8 +854,7 @@ fn scrub_notch_labels_carry_helper_strings() {
|
|||||||
|
|
||||||
let mut texts = scrub_notch_label_texts(&mut app);
|
let mut texts = scrub_notch_label_texts(&mut app);
|
||||||
texts.sort();
|
texts.sort();
|
||||||
let mut expected: Vec<String> =
|
let mut expected: Vec<String> = scrub_notch_labels().iter().map(|s| s.to_string()).collect();
|
||||||
scrub_notch_labels().iter().map(|s| s.to_string()).collect();
|
|
||||||
expected.sort();
|
expected.sort();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
texts, expected,
|
texts, expected,
|
||||||
@@ -1106,10 +1105,22 @@ fn move_log_active_row_text(app: &mut App) -> String {
|
|||||||
#[test]
|
#[test]
|
||||||
fn format_pile_uses_one_indexed_lowercase_names() {
|
fn format_pile_uses_one_indexed_lowercase_names() {
|
||||||
assert_eq!(format_pile(&KlondikePile::Stock), "waste");
|
assert_eq!(format_pile(&KlondikePile::Stock), "waste");
|
||||||
assert_eq!(format_pile(&KlondikePile::Foundation(Foundation::Foundation1)), "foundation 1");
|
assert_eq!(
|
||||||
assert_eq!(format_pile(&KlondikePile::Foundation(Foundation::Foundation3)), "foundation 3");
|
format_pile(&KlondikePile::Foundation(Foundation::Foundation1)),
|
||||||
assert_eq!(format_pile(&KlondikePile::Tableau(Tableau::Tableau1)), "tableau 1");
|
"foundation 1"
|
||||||
assert_eq!(format_pile(&KlondikePile::Tableau(Tableau::Tableau7)), "tableau 7");
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format_pile(&KlondikePile::Foundation(Foundation::Foundation3)),
|
||||||
|
"foundation 3"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format_pile(&KlondikePile::Tableau(Tableau::Tableau1)),
|
||||||
|
"tableau 1"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
format_pile(&KlondikePile::Tableau(Tableau::Tableau7)),
|
||||||
|
"tableau 7"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move-body formatter renders `StockClick` as a label and
|
/// Move-body formatter renders `StockClick` as a label and
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use super::format::{
|
use super::format::{
|
||||||
format_active_move_row, format_foundations_row, format_kth_next_row,
|
format_active_move_row, format_foundations_row, format_kth_next_row, format_kth_recent_row,
|
||||||
format_kth_recent_row, format_move_log_header, format_progress, format_stock_waste_row,
|
format_move_log_header, format_progress, format_stock_waste_row,
|
||||||
};
|
};
|
||||||
|
use super::*;
|
||||||
use crate::layout::LayoutResource;
|
use crate::layout::LayoutResource;
|
||||||
use crate::replay_playback::ReplayPlaybackState;
|
use crate::replay_playback::ReplayPlaybackState;
|
||||||
use crate::resources::GameStateResource;
|
use crate::resources::GameStateResource;
|
||||||
|
|||||||
@@ -268,11 +268,12 @@ pub fn step_replay_playback(
|
|||||||
}
|
}
|
||||||
match &replay.moves[*cursor] {
|
match &replay.moves[*cursor] {
|
||||||
ReplayMove::Move { from, to, count } => {
|
ReplayMove::Move { from, to, count } => {
|
||||||
let (Ok(from), Ok(to)) = (
|
let (Ok(from), Ok(to)) = (KlondikePile::try_from(*from), KlondikePile::try_from(*to))
|
||||||
KlondikePile::try_from(*from),
|
else {
|
||||||
KlondikePile::try_from(*to),
|
warn!(
|
||||||
) else {
|
"skipping replay move with invalid pile encoding at cursor {}",
|
||||||
warn!("skipping replay move with invalid pile encoding at cursor {}", *cursor);
|
*cursor
|
||||||
|
);
|
||||||
*cursor += 1;
|
*cursor += 1;
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -379,10 +380,9 @@ fn tick_replay_playback(
|
|||||||
while *secs_to_next <= 0.0 && *cursor < replay.moves.len() {
|
while *secs_to_next <= 0.0 && *cursor < replay.moves.len() {
|
||||||
match &replay.moves[*cursor] {
|
match &replay.moves[*cursor] {
|
||||||
ReplayMove::Move { from, to, count } => {
|
ReplayMove::Move { from, to, count } => {
|
||||||
if let (Ok(from), Ok(to)) = (
|
if let (Ok(from), Ok(to)) =
|
||||||
KlondikePile::try_from(*from),
|
(KlondikePile::try_from(*from), KlondikePile::try_from(*to))
|
||||||
KlondikePile::try_from(*to),
|
{
|
||||||
) {
|
|
||||||
moves_writer.write(MoveRequestEvent {
|
moves_writer.write(MoveRequestEvent {
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use std::sync::Arc;
|
|||||||
use bevy::math::Vec2;
|
use bevy::math::Vec2;
|
||||||
use bevy::prelude::Resource;
|
use bevy::prelude::Resource;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use solitaire_core::game_state::GameState;
|
|
||||||
use klondike::KlondikePile;
|
use klondike::KlondikePile;
|
||||||
|
use solitaire_core::game_state::GameState;
|
||||||
|
|
||||||
/// Wraps the currently active `GameState`. Single source of truth for the in-progress game.
|
/// Wraps the currently active `GameState`. Single source of truth for the in-progress game.
|
||||||
#[derive(Resource, Debug, Clone)]
|
#[derive(Resource, Debug, Clone)]
|
||||||
|
|||||||
@@ -202,7 +202,10 @@ fn cycled_piles() -> Vec<KlondikePile> {
|
|||||||
///
|
///
|
||||||
/// If `current` is `None` the first available pile is returned.
|
/// If `current` is `None` the first available pile is returned.
|
||||||
/// If `available` is empty, `None` is returned.
|
/// If `available` is empty, `None` is returned.
|
||||||
pub fn cycle_next_pile(available: &[KlondikePile], current: Option<&KlondikePile>) -> Option<KlondikePile> {
|
pub fn cycle_next_pile(
|
||||||
|
available: &[KlondikePile],
|
||||||
|
current: Option<&KlondikePile>,
|
||||||
|
) -> Option<KlondikePile> {
|
||||||
if available.is_empty() {
|
if available.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -235,7 +238,11 @@ pub fn cycle_next_pile(available: &[KlondikePile], current: Option<&KlondikePile
|
|||||||
///
|
///
|
||||||
/// Both `current` and `next` must be `Some`; if either is `None` this returns
|
/// Both `current` and `next` must be `Some`; if either is `None` this returns
|
||||||
/// `false`.
|
/// `false`.
|
||||||
fn did_wrap(available: &[KlondikePile], current: Option<&KlondikePile>, next: Option<&KlondikePile>) -> bool {
|
fn did_wrap(
|
||||||
|
available: &[KlondikePile],
|
||||||
|
current: Option<&KlondikePile>,
|
||||||
|
next: Option<&KlondikePile>,
|
||||||
|
) -> bool {
|
||||||
let (Some(cur), Some(nxt)) = (current, next) else {
|
let (Some(cur), Some(nxt)) = (current, next) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -386,9 +393,7 @@ fn handle_selection_keys(
|
|||||||
KlondikePile::Tableau(Tableau::Tableau7),
|
KlondikePile::Tableau(Tableau::Tableau7),
|
||||||
];
|
];
|
||||||
all.into_iter()
|
all.into_iter()
|
||||||
.filter(|p| {
|
.filter(|p| pile_cards(&game.0, p).last().is_some_and(|c| c.face_up))
|
||||||
pile_cards(&game.0, p).last().is_some_and(|c| c.face_up)
|
|
||||||
})
|
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -717,10 +722,7 @@ fn update_selection_highlight(
|
|||||||
|
|
||||||
/// Returns the top face-up card on `pile`, or `None` if the pile is
|
/// Returns the top face-up card on `pile`, or `None` if the pile is
|
||||||
/// empty or its top card is face-down.
|
/// empty or its top card is face-down.
|
||||||
fn top_face_up_card(
|
fn top_face_up_card(pile: &KlondikePile, game: &GameState) -> Option<Card> {
|
||||||
pile: &KlondikePile,
|
|
||||||
game: &GameState,
|
|
||||||
) -> Option<Card> {
|
|
||||||
pile_cards(game, pile).last().filter(|c| c.face_up).cloned()
|
pile_cards(game, pile).last().filter(|c| c.face_up).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1162,7 +1164,10 @@ mod tests {
|
|||||||
// DragState must mirror the lifted cards and carry the keyboard sentinel.
|
// DragState must mirror the lifted cards and carry the keyboard sentinel.
|
||||||
let drag = app.world().resource::<DragState>();
|
let drag = app.world().resource::<DragState>();
|
||||||
assert_eq!(drag.cards, vec![100]);
|
assert_eq!(drag.cards, vec![100]);
|
||||||
assert_eq!(drag.origin_pile, Some(KlondikePile::Tableau(Tableau::Tableau1)));
|
assert_eq!(
|
||||||
|
drag.origin_pile,
|
||||||
|
Some(KlondikePile::Tableau(Tableau::Tableau1))
|
||||||
|
);
|
||||||
assert_eq!(drag.active_touch_id, Some(KEYBOARD_DRAG_TOUCH_ID));
|
assert_eq!(drag.active_touch_id, Some(KEYBOARD_DRAG_TOUCH_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ use crate::events::{
|
|||||||
use crate::font_plugin::FontResource;
|
use crate::font_plugin::FontResource;
|
||||||
use crate::progress_plugin::ProgressResource;
|
use crate::progress_plugin::ProgressResource;
|
||||||
use crate::resources::{SettingsScrollPos, SyncStatus, SyncStatusResource};
|
use crate::resources::{SettingsScrollPos, SyncStatus, SyncStatusResource};
|
||||||
use crate::theme::{ThemeThumbnailCache, ThemeThumbnailPair};
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use crate::theme::{ImportError, import_theme, refresh_registry};
|
use crate::theme::{ImportError, import_theme, refresh_registry};
|
||||||
|
use crate::theme::{ThemeThumbnailCache, ThemeThumbnailPair};
|
||||||
use crate::ui_focus::{FocusGroup, FocusRow, Focusable, FocusedButton};
|
use crate::ui_focus::{FocusGroup, FocusRow, Focusable, FocusedButton};
|
||||||
use crate::ui_modal::{
|
use crate::ui_modal::{
|
||||||
ButtonVariant, ModalButton, ModalScrim, spawn_modal, spawn_modal_actions, spawn_modal_button,
|
ButtonVariant, ModalButton, ModalScrim, spawn_modal, spawn_modal_actions, spawn_modal_button,
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use bevy::log::warn;
|
use bevy::log::warn;
|
||||||
use bevy::prelude::{App, Plugin, Resource};
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
use bevy::prelude::Startup;
|
use bevy::prelude::Startup;
|
||||||
|
use bevy::prelude::{App, Plugin, Resource};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::ThemeMeta;
|
use super::ThemeMeta;
|
||||||
|
|||||||
@@ -249,11 +249,11 @@ fn build_router_inner(state: AppState, rate_limit: bool) -> Router {
|
|||||||
|
|
||||||
const CSP: &str = concat!(
|
const CSP: &str = concat!(
|
||||||
"default-src 'self'; ",
|
"default-src 'self'; ",
|
||||||
"script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; ",
|
"script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://analytics.aleshym.co; ",
|
||||||
"style-src 'self' 'unsafe-inline'; ",
|
"style-src 'self' 'unsafe-inline'; ",
|
||||||
"font-src 'self'; ",
|
"font-src 'self'; ",
|
||||||
"img-src 'self' data:; ",
|
"img-src 'self' data: https://analytics.aleshym.co; ",
|
||||||
"connect-src 'self'; ",
|
"connect-src 'self' https://analytics.aleshym.co; ",
|
||||||
"object-src 'none'; ",
|
"object-src 'none'; ",
|
||||||
"frame-ancestors 'none'",
|
"frame-ancestors 'none'",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -196,10 +196,7 @@ async fn update_leaderboard_if_opted_in(
|
|||||||
user_id: &str,
|
user_id: &str,
|
||||||
payload: &SyncPayload,
|
payload: &SyncPayload,
|
||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let opted_in = sqlx::query!(
|
let opted_in = sqlx::query!("SELECT leaderboard_opt_in FROM users WHERE id = ?", user_id)
|
||||||
"SELECT leaderboard_opt_in FROM users WHERE id = ?",
|
|
||||||
user_id
|
|
||||||
)
|
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await?
|
.await?
|
||||||
.map(|r| r.leaderboard_opt_in)
|
.map(|r| r.leaderboard_opt_in)
|
||||||
|
|||||||
Reference in New Issue
Block a user