Compare commits
152 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27cdf78ce0 | |||
| faa6c5efc4 | |||
| 487b99bbc9 | |||
| 53e3b816cf | |||
| 87275bf340 | |||
| 56647d7f0d | |||
| cbf2483028 | |||
| a54201e97b | |||
| 48e412177c | |||
| cd54ce1bb0 | |||
| 7a3032b74c | |||
| 89699a8a86 | |||
| 70165da103 | |||
| 8a5fa8751c | |||
| bf660df971 | |||
| 13a8a012ee | |||
| 02ababa65f | |||
| 9c36b49729 | |||
| 8e90574437 | |||
| 95fcdad5d2 | |||
| d948fa862a | |||
| 1fcd032b0a | |||
| 3081505a3d | |||
| 07b8ecd9b2 | |||
| 5bed43ef32 | |||
| 23c9704887 | |||
| 93182fa251 | |||
| 89c51ab712 | |||
| 3984231c9b | |||
| d9f36bf34a | |||
| 57d1c58fdf | |||
| 42535f5109 | |||
| d5e6f8026b | |||
| 271647265c | |||
| 3eabc149a8 | |||
| f1aeb24157 | |||
| 000143231b | |||
| 1a1047664b | |||
| ba527de351 | |||
| fe41b502ac | |||
| b37f0cbec7 | |||
| a0fc0d2605 | |||
| 7ed4f2cba9 | |||
| ddc8f27c82 | |||
| 13dd44bd1b | |||
| 17f9b518f1 | |||
| 61d891fb76 | |||
| 7dba772e67 | |||
| ca5788f714 | |||
| 9887343d8b | |||
| 525fe0fe76 | |||
| 69ce9afab9 | |||
| 13aa0fd833 | |||
| 9f095c4039 | |||
| d8c70341f4 | |||
| 063269c70e | |||
| b126df82b2 | |||
| 655dfde736 | |||
| f712b89fe4 | |||
| f6c916641a | |||
| 95df5421c9 | |||
| fdb6c2ecfe | |||
| 9a3d7f3876 | |||
| c4970b16ea | |||
| 2c72e1fc87 | |||
| efa063fb8f | |||
| 78cf30e906 | |||
| 9a9026e33a | |||
| ab1d098877 | |||
| 160637d1c8 | |||
| 43f13c615e | |||
| 924a1e2af7 | |||
| a6b8348332 | |||
| b98cb8a99f | |||
| 7b59e70192 | |||
| 7f477b4ad8 | |||
| ce38b26721 | |||
| 172d7773f0 | |||
| 205ad6f646 | |||
| 936d035750 | |||
| 13d1d013e9 | |||
| b8fb3fbd6e | |||
| e510e90b95 | |||
| 902560cd68 | |||
| 912b08c719 | |||
| 3ef4ecb747 | |||
| 4b9d008be2 | |||
| 74482252d1 | |||
| 6e7705b256 | |||
| 59316de1e9 | |||
| 1719fdada0 | |||
| 8dda9541a3 | |||
| 60a80369d4 | |||
| dbe6c60133 | |||
| 74597a8c84 | |||
| 5d57b67934 | |||
| 220e3f040c | |||
| 54d34972d4 | |||
| 0c86cac2d5 | |||
| 2e080d02ce | |||
| 73e210b243 | |||
| f866299021 | |||
| b78a493a0c | |||
| 51d3454344 | |||
| 12789529a1 | |||
| c1bde18a2c | |||
| fd7fb7b6da | |||
| 138436558f | |||
| 65d595ad12 | |||
| abeb4e5cdf | |||
| b082bd65a6 | |||
| de52c8a7b7 | |||
| dcfa976dad | |||
| 71999e1062 | |||
| 5f5aba8dff | |||
| 9bfca929cb | |||
| 534870a68a | |||
| 0066ca6205 | |||
| 54e024c1b0 | |||
| 3a01318fbd | |||
| 79d391724e | |||
| ba019c0ba7 | |||
| 18d7c121a3 | |||
| cb93bd9265 | |||
| 6723416a55 | |||
| afb08799e8 | |||
| 3b619b8950 | |||
| 37681cf33e | |||
| 99064ce808 | |||
| de4dba6f98 | |||
| 75fc3aa3d6 | |||
| deb034c5fb | |||
| 242b5fef21 | |||
| 3f922ede28 | |||
| 8da62bd05f | |||
| 73cad7e205 | |||
| e14852c093 | |||
| 6240156fee | |||
| 1d9fb1884a | |||
| 97f38085e3 | |||
| 62cd1cf924 | |||
| b10e1a5a87 | |||
| 366fd6d127 | |||
| 7a77c66f6d | |||
| adece12cf1 | |||
| 2cfbc32715 | |||
| 56b37fc653 | |||
| 3ffde038c5 | |||
| ece2a55ffb | |||
| abda354562 | |||
| fbe984cf64 | |||
| efec6f22d5 |
@@ -1,4 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
|
/.sccache-cache
|
||||||
*.db
|
*.db
|
||||||
*.db-shm
|
*.db-shm
|
||||||
*.db-wal
|
*.db-wal
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT\n r.id AS \"id!: String\",\n u.username AS \"username!: String\",\n r.seed AS \"seed!: i64\",\n r.draw_mode AS \"draw_mode!: String\",\n r.mode AS \"mode!: String\",\n r.time_seconds AS \"time_seconds!: i64\",\n r.final_score AS \"final_score!: i64\",\n r.recorded_at AS \"recorded_at!: String\",\n r.received_at AS \"received_at!: String\"\n FROM replays r\n JOIN users u ON u.id = r.user_id\n ORDER BY r.received_at DESC\n LIMIT ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id!: String",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "username!: String",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "seed!: i64",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "draw_mode!: String",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mode!: String",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "time_seconds!: i64",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "final_score!: i64",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "recorded_at!: String",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "received_at!: String",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "3a9bd2e51b2389da5b7e85f26806fcffa896748e0b589d216cf60827fc3857a9"
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT replay_json FROM replays WHERE id = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "replay_json",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "5bc1984044bc792c2e9577a159ca22789469df14cb25144451f37e8cdad8165c"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO replays (\n id, user_id, seed, draw_mode, mode, time_seconds, final_score,\n recorded_at, received_at, replay_json\n ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 10
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "6a36a96faa9d9b423aae3b72b0c049a1489b67ca2361581b2300bb4ee0bc9e2f"
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ Solitaire Quest is a cross-platform Klondike Solitaire game written in Rust, tar
|
|||||||
- **No panics in game logic.** Every state transition returns `Result<_, MoveError>`. Panics are only acceptable in startup/configuration code.
|
- **No panics in game logic.** Every state transition returns `Result<_, MoveError>`. Panics are only acceptable in startup/configuration code.
|
||||||
- **One language, one repo.** The game client, sync client, shared types, and sync server are all Rust crates in a single Cargo workspace.
|
- **One language, one repo.** The game client, sync client, shared types, and sync server are all Rust crates in a single Cargo workspace.
|
||||||
- **Plugin-based Bevy architecture.** Each major feature is a Bevy `Plugin`. Systems are small and single-purpose. Cross-system communication uses Bevy `Event`s.
|
- **Plugin-based Bevy architecture.** Each major feature is a Bevy `Plugin`. Systems are small and single-purpose. Cross-system communication uses Bevy `Event`s.
|
||||||
|
- **UI-first interaction.** Every player-triggered action — new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, etc. — must be reachable from a visible UI control. Keyboard shortcuts exist only as optional accelerators for power users; they are never the sole entry point. A player using only mouse or touch must be able to perform every action. New gameplay features ship with the UI control alongside the system that backs it.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -67,11 +68,11 @@ solitaire_quest/
|
|||||||
├── Dockerfile # Multi-stage server build
|
├── Dockerfile # Multi-stage server build
|
||||||
├── docker-compose.yml # Server + Caddy reverse proxy
|
├── docker-compose.yml # Server + Caddy reverse proxy
|
||||||
│
|
│
|
||||||
├── assets/ # Assets embedded at compile time via include_bytes!()
|
├── assets/ # Loaded at runtime via AssetServer (audio is embedded via include_bytes!())
|
||||||
│ ├── cards/
|
│ ├── cards/
|
||||||
│ │ ├── faces/{rank}_{suit}.png # 52 individual card faces (120×168, generated by solitaire_assetgen)
|
│ │ ├── faces/{RANK}{SUIT}.png # 52 card faces — rendered from hayeah/playing-cards-assets SVGs (MIT)
|
||||||
│ │ └── backs/back_0.png – back_4.png # placeholder patterns
|
│ │ └── backs/back_0.png – back_4.png # back_0 = generated default back; back_1–4 are generated patterns
|
||||||
│ ├── backgrounds/bg_0.png – bg_4.png # placeholder textures
|
│ ├── backgrounds/bg_0.png – bg_4.png # generated textures
|
||||||
│ ├── fonts/main.ttf # FiraMono-Medium (170K, OFL)
|
│ ├── fonts/main.ttf # FiraMono-Medium (170K, OFL)
|
||||||
│ └── audio/
|
│ └── audio/
|
||||||
│ ├── card_deal.wav
|
│ ├── card_deal.wav
|
||||||
@@ -132,7 +133,7 @@ Owns:
|
|||||||
- `SyncProvider` trait — implemented by `SolitaireServerClient`
|
- `SyncProvider` trait — implemented by `SolitaireServerClient`
|
||||||
|
|
||||||
### `solitaire_engine`
|
### `solitaire_engine`
|
||||||
**Dependencies:** `bevy`, `bevy_kira_audio`, `solitaire_core`, `solitaire_data`.
|
**Dependencies:** `bevy`, `kira`, `solitaire_core`, `solitaire_data`.
|
||||||
|
|
||||||
All Bevy-specific code. Structured as a collection of Plugins that `solitaire_app` registers.
|
All Bevy-specific code. Structured as a collection of Plugins that `solitaire_app` registers.
|
||||||
|
|
||||||
@@ -144,7 +145,7 @@ Owns:
|
|||||||
- All Bevy UI screens (Home, Stats, Achievements, Settings, Profile)
|
- All Bevy UI screens (Home, Stats, Achievements, Settings, Profile)
|
||||||
- Audio playback systems
|
- Audio playback systems
|
||||||
- Sync status display
|
- Sync status display
|
||||||
- Card, background, and font asset loading (embedded via `include_bytes!()` — no `AssetServer` dependency)
|
- Card, background, and font asset loading via Bevy `AssetServer` (audio is the lone exception — embedded via `include_bytes!()` in `audio_plugin.rs`)
|
||||||
|
|
||||||
### `solitaire_server`
|
### `solitaire_server`
|
||||||
**Dependencies:** `solitaire_sync`, `axum`, `sqlx`, `jsonwebtoken`, `bcrypt`, `tower-governor`, `tracing`, `tokio`, `dotenvy`.
|
**Dependencies:** `solitaire_sync`, `axum`, `sqlx`, `jsonwebtoken`, `bcrypt`, `tower-governor`, `tracing`, `tokio`, `dotenvy`.
|
||||||
@@ -235,20 +236,22 @@ Done
|
|||||||
|
|
||||||
### Bevy Plugins
|
### Bevy Plugins
|
||||||
|
|
||||||
| Plugin | Key | Responsibility |
|
The "Shortcut" column lists optional keyboard accelerators. Every action in this table must also be reachable from a visible UI control (button, menu item, on-screen affordance) per the UI-first design principle in §1; the shortcut is a power-user convenience, not the sole entry point.
|
||||||
|
|
||||||
|
| Plugin | Shortcut | Responsibility |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `CardPlugin` | — | Card entity spawning, sprite management, drag-and-drop |
|
| `CardPlugin` | — | Card entity spawning, sprite management, drag-and-drop |
|
||||||
| `TablePlugin` | — | Pile markers, background, layout calculation |
|
| `TablePlugin` | — | Pile markers, background, layout calculation |
|
||||||
| `FontPlugin` | — | Embeds FiraMono-Medium font at compile time; exposes `FontResource` handle |
|
| `FontPlugin` | — | Loads FiraMono-Medium via `AssetServer` at startup; exposes `FontResource` handle |
|
||||||
| `AnimationPlugin` | — | Slide, flip, win cascade, toast animations |
|
| `AnimationPlugin` | — | Slide, flip, win cascade, toast animations |
|
||||||
| `FeedbackAnimPlugin` | — | Shake, settle, and deal-stagger animations |
|
| `FeedbackAnimPlugin` | — | Shake, settle, and deal-stagger animations |
|
||||||
| `AutoCompletePlugin` | Enter | Executes auto-complete when the HUD badge is lit |
|
| `AutoCompletePlugin` | Enter | Executes auto-complete when the HUD badge is lit |
|
||||||
| `AudioPlugin` | — | Sound effect and music playback via bevy_kira_audio |
|
| `AudioPlugin` | — | Sound effect and music playback via kira |
|
||||||
| `InputPlugin` | — | Keyboard and mouse input routing |
|
| `InputPlugin` | — | Keyboard and mouse input routing |
|
||||||
| `CursorPlugin` | — | Custom cursor sprite during drag |
|
| `CursorPlugin` | — | Custom cursor sprite during drag |
|
||||||
| `SelectionPlugin` | — | Keyboard-driven card selection |
|
| `SelectionPlugin` | — | Keyboard-driven card selection |
|
||||||
| `GamePlugin` | N | Core game state resource, new-game flow, win/game-over overlays |
|
| `GamePlugin` | N | Core game state resource, new-game flow, win/game-over overlays |
|
||||||
| `HudPlugin` | — | Score, move counter, timer, auto-complete badge |
|
| `HudPlugin` | — | Score, move counter, timer, auto-complete badge, and the top-right action button bar (Undo / Pause / Help / New Game). Each button fires the same request event the corresponding hotkey does. |
|
||||||
| `StatsPlugin` | S | Stats overlay and persistence |
|
| `StatsPlugin` | S | Stats overlay and persistence |
|
||||||
| `ProgressPlugin` | — | XP/level system, persistence |
|
| `ProgressPlugin` | — | XP/level system, persistence |
|
||||||
| `AchievementPlugin` | A | Unlock evaluation, toast events, persistence |
|
| `AchievementPlugin` | A | Unlock evaluation, toast events, persistence |
|
||||||
@@ -296,7 +299,7 @@ struct CardImageSet {
|
|||||||
backs: [Handle<Image>; 5], // indexed by selected_card_back setting
|
backs: [Handle<Image>; 5], // indexed by selected_card_back setting
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project-wide font handle (FiraMono-Medium embedded at compile time)
|
// Project-wide font handle (FiraMono-Medium loaded via AssetServer at startup)
|
||||||
struct FontResource(Handle<Font>);
|
struct FontResource(Handle<Font>);
|
||||||
|
|
||||||
// Pre-loaded background PNG handles
|
// Pre-loaded background PNG handles
|
||||||
@@ -713,11 +716,14 @@ pub struct AchievementDef {
|
|||||||
| `speed_and_skill` | ??? | Win < 90s without undo | Yes | Card back #4 |
|
| `speed_and_skill` | ??? | Win < 90s without undo | Yes | Card back #4 |
|
||||||
| `comeback` | ??? | Win after 3+ stock recycles | Yes | Background #4 |
|
| `comeback` | ??? | Win after 3+ stock recycles | Yes | Background #4 |
|
||||||
| `zen_winner` | ??? | Win in Zen Mode | Yes | Badge |
|
| `zen_winner` | ??? | Win in Zen Mode | Yes | Badge |
|
||||||
|
| `cinephile` | Cinephile | Watch a saved replay all the way through | No | — |
|
||||||
|
|
||||||
### Evaluation Timing
|
### Evaluation Timing
|
||||||
|
|
||||||
Achievement conditions are evaluated by `AchievementPlugin` on every `GameWonEvent` and `StateChangedEvent`. The plugin calls `solitaire_core::check_achievements()` which returns a `Vec<AchievementDef>` of newly unlocked achievements. The plugin then fires `AchievementUnlockedEvent` for each, which the toast and persistence systems handle independently.
|
Achievement conditions are evaluated by `AchievementPlugin` on every `GameWonEvent` and `StateChangedEvent`. The plugin calls `solitaire_core::check_achievements()` which returns a `Vec<AchievementDef>` of newly unlocked achievements. The plugin then fires `AchievementUnlockedEvent` for each, which the toast and persistence systems handle independently.
|
||||||
|
|
||||||
|
A small number of achievements are *event-driven* rather than condition-driven: their `AchievementDef::condition` always returns `false` and their unlock is written from a dedicated observer system instead. `cinephile` is the canonical example — it unlocks when `ReplayPlaybackState` transitions from `Playing` to `Completed` (a saved replay watched to its natural end). The Stop button transitions `Playing → Inactive` directly without entering `Completed`, so manual aborts do not unlock the achievement.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. Progression System
|
## 12. Progression System
|
||||||
@@ -751,7 +757,7 @@ Levels 11+: level = 10 + floor((total_xp - 5000) / 1000)
|
|||||||
|
|
||||||
## 13. Audio System
|
## 13. Audio System
|
||||||
|
|
||||||
Audio uses `bevy_kira_audio`. All sound files are `.wav`.
|
Audio uses `kira`. All sound files are `.wav`.
|
||||||
|
|
||||||
| File | Trigger |
|
| File | Trigger |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -762,7 +768,7 @@ Audio uses `bevy_kira_audio`. All sound files are `.wav`.
|
|||||||
| `win_fanfare.wav` | Game won |
|
| `win_fanfare.wav` | Game won |
|
||||||
| `ambient_loop.wav` | Looping background music |
|
| `ambient_loop.wav` | Looping background music |
|
||||||
|
|
||||||
Volume is controlled by two independent sliders in Settings (`sfx_volume`, `music_volume`), each stored in `Settings` and applied as `bevy_kira_audio` channel volumes.
|
Volume is controlled by two independent sliders in Settings (`sfx_volume`, `music_volume`), each stored in `Settings` and applied as `kira` channel volumes.
|
||||||
|
|
||||||
Audio systems listen for Bevy events and never block the game thread.
|
Audio systems listen for Bevy events and never block the game thread.
|
||||||
|
|
||||||
@@ -772,11 +778,13 @@ Audio systems listen for Bevy events and never block the game thread.
|
|||||||
|
|
||||||
### Rendering approach
|
### Rendering approach
|
||||||
|
|
||||||
Cards are Bevy `Sprite` entities with `Handle<Image>` from `CardImageSet`. Face-up cards use one of 52 individual face PNGs selected by `faces[suit][rank]` — rank and suit are baked into each image and no `Text2d` overlay is spawned. Face-down cards use `backs/back_N.png` indexed by `settings.selected_card_back`. `Text2d` labels are only used as a fallback when `CardImageSet` is absent (e.g. tests with `MinimalPlugins`). `CardImageSet` is populated at startup from `include_bytes!()` — no `AssetServer`.
|
Cards are Bevy `Sprite` entities with `Handle<Image>` from `CardImageSet`. Face-up cards use one of 52 individual face PNGs selected by `faces[suit][rank]` — rank and suit are baked into each image and no `Text2d` overlay is spawned. Face-down cards use `backs/back_N.png` indexed by `settings.selected_card_back`. `Text2d` labels are only used as a fallback when `CardImageSet` is absent (e.g. tests with `MinimalPlugins`). `CardImageSet` is populated at startup by `card_plugin::load_card_images` via `AssetServer::load()`.
|
||||||
|
|
||||||
Backgrounds are Bevy `Sprite` entities with `Handle<Image>` from `BackgroundImageSet`. `BackgroundImageSet` is populated at startup from `include_bytes!()`.
|
Backgrounds are Bevy `Sprite` entities with `Handle<Image>` from `BackgroundImageSet`. `BackgroundImageSet` is populated at startup by `table_plugin::load_background_images` via `AssetServer::load()`.
|
||||||
|
|
||||||
The font `FiraMono-Medium` is embedded via `include_bytes!()` at startup by `FontPlugin` and exposed as `FontResource` for use by all UI and text systems.
|
The font `FiraMono-Medium` is loaded via `AssetServer::load("fonts/main.ttf")` at startup by `FontPlugin` and exposed as `FontResource` for use by all UI and text systems.
|
||||||
|
|
||||||
|
All three loaders take `Option<Res<AssetServer>>` so they degrade cleanly under `MinimalPlugins` in tests: when the server is absent, `CardImageSet`/`BackgroundImageSet` are inserted with empty handle slots and the plugins fall back to `Text2d` rank+suit overlays and solid-colour board backgrounds. The `assets/` directory must ship alongside the binary.
|
||||||
|
|
||||||
The `assets/` directory layout:
|
The `assets/` directory layout:
|
||||||
|
|
||||||
@@ -1004,5 +1012,7 @@ Using `axum::test` and an in-memory SQLite database:
|
|||||||
| `SyncProvider` trait, not `SyncBackend` match arms | `SyncPlugin` stays backend-agnostic and testable; new backends can be added without touching the plugin | 2026-04-20 |
|
| `SyncProvider` trait, not `SyncBackend` match arms | `SyncPlugin` stays backend-agnostic and testable; new backends can be added without touching the plugin | 2026-04-20 |
|
||||||
| Dropped WebDAV backend | Redundant once the self-hosted server exists; removing it reduces surface area and simplifies settings UI | 2026-04-20 |
|
| Dropped WebDAV backend | Redundant once the self-hosted server exists; removing it reduces surface area and simplifies settings UI | 2026-04-20 |
|
||||||
| Dropped GPGS backend | Redundant with the self-hosted server; adds JNI complexity for no user-visible benefit on the target platforms | 2026-04-28 |
|
| Dropped GPGS backend | Redundant with the self-hosted server; adds JNI complexity for no user-visible benefit on the target platforms | 2026-04-28 |
|
||||||
| PNG assets embedded via `include_bytes!()` | Using `Image::from_buffer()` in startup systems rather than `AssetServer::load()` keeps the binary self-contained and eliminates runtime file-not-found errors | 2026-04-29 |
|
| Card, background, and font assets loaded via `AssetServer` | Reverses the earlier embed-via-`include_bytes!()` decision: PNGs and TTFs are loaded at runtime so artwork can be swapped (e.g. alternate card backs, themed backgrounds) without a recompile, and binary size stays small. Loaders take `Option<Res<AssetServer>>` and fall back gracefully under `MinimalPlugins`. The `assets/` directory must ship alongside the binary. | 2026-04-29 |
|
||||||
| FiraMono-Medium font embedded via `include_bytes!()` | Exposed through `FontResource`; avoids runtime font loading errors on headless systems and ensures consistent text rendering across all platforms | 2026-04-29 |
|
| Audio assets remain embedded via `include_bytes!()` | Audio files are small, change rarely, and the embedded path eliminates a class of runtime-load errors during gameplay; the asset-pipeline reversal does not extend to audio | 2026-04-29 |
|
||||||
|
| Card art swapped from xCards (LGPL-3.0) to hayeah/playing-cards-assets (MIT) | Public-release readiness. The previous xCards art carried LGPL relinking obligations that complicate a single-binary distribution; hayeah's set derives from the public-domain `vector-playing-cards` line-art and is permissively MIT-licensed. CREDITS.md license summary collapsed to MIT + OFL-1.1. The default card back is original work in this project's midnight-purple palette. | 2026-05-01 |
|
||||||
|
| Runtime SVG card-theme system (`CARD_PLAN.md`) | User-supplied themes need to ship SVG sources so they can rasterise at any resolution on the player's hardware; baking PNGs at build time only would lock theme installation to the developer. The pipeline (usvg → resvg → tiny-skia) rasterises once per (theme, target size) at load time and caches the resulting `Image`, so the runtime cost is paid once, not per frame. The bundled default theme ships via `embedded://`; user themes via `themes://` rooted at `user_theme_dir()`. | 2026-05-01 |
|
||||||
|
|||||||
@@ -0,0 +1,568 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to Solitaire Quest are documented here. The format is
|
||||||
|
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
|
||||||
|
project follows [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
_Nothing yet._
|
||||||
|
|
||||||
|
## [0.17.0] — 2026-05-06
|
||||||
|
|
||||||
|
A short follow-up round on top of v0.16.0: the H-key hint is no
|
||||||
|
longer a heuristic guess but the actual best first move suggested by
|
||||||
|
the v0.15.0 solver, and the in-engine replay player now has a
|
||||||
|
player-tunable playback rate.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Replay-rate slider** in Settings → Gameplay. Tunes
|
||||||
|
`replay_move_interval_secs` from 0.10 s to 1.00 s in 0.05 s steps;
|
||||||
|
default 0.45 s. `tick_replay_playback` reads the value from
|
||||||
|
`SettingsResource` per frame so the slider takes effect on the
|
||||||
|
next playback tick — no restart required.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Solver-driven hints.** Pressing **H** used to surface a
|
||||||
|
heuristic-best move (foundation moves preferred, then
|
||||||
|
tableau-to-tableau by depth-of-flip-revealed). It now asks the
|
||||||
|
v0.15.0 solver for the actual provably-best first move via the
|
||||||
|
new `solitaire_core::solver::try_solve_with_first_move` /
|
||||||
|
`try_solve_from_state` APIs. When the solver returns inconclusive
|
||||||
|
(rare deals where the bound runs out before a result), the old
|
||||||
|
heuristic remains the fallback. Median 2 ms per H press.
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
|
||||||
|
- 1208 passing tests (was 1196 at v0.16.0 close).
|
||||||
|
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||||
|
|
||||||
|
## [0.16.0] — 2026-05-06
|
||||||
|
|
||||||
|
A modal-feel polish round. Every overlay screen now scrolls when its
|
||||||
|
content overflows the 800×600 minimum window, every clickable button
|
||||||
|
shows a hand cursor on hover, keyboard focus lands on the primary
|
||||||
|
button on the same frame the modal opens, and read-only modals
|
||||||
|
dismiss when the player clicks the scrim outside the card.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Pointer cursor on hover** for every interactive `Button` entity
|
||||||
|
(modal buttons, HUD action bar, mode-launcher cards, settings
|
||||||
|
toggles, Stats selectors). `update_cursor_icon` gains a fourth
|
||||||
|
branch sitting between Grabbing (active drag) and Grab
|
||||||
|
(draggable card hover): when no drag is active and any
|
||||||
|
`Interaction::Hovered`/`Pressed` button is detected, the window
|
||||||
|
cursor swaps to `SystemCursorIcon::Pointer`. A pure
|
||||||
|
`pick_cursor_icon` helper makes the priority logic
|
||||||
|
unit-testable.
|
||||||
|
- **Click-outside-to-dismiss** for the six read-only modals: Stats,
|
||||||
|
Achievements, Help, Profile, Leaderboard, Home. New
|
||||||
|
`ScrimDismissible` marker on `ModalScrim` opts a modal in;
|
||||||
|
`dismiss_modal_on_scrim_click` runs in `Update`, despawns the
|
||||||
|
topmost dismissible scrim on a left-mouse press whose cursor
|
||||||
|
lands on the scrim and outside every `ModalCard`. Bevy's
|
||||||
|
hierarchy despawn cascades to the card and children.
|
||||||
|
Settings, Onboarding, Pause, Forfeit confirm, and Confirm New
|
||||||
|
Game intentionally don't opt in — they carry unsaved or
|
||||||
|
destructive state.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Modal content scrolls when it overflows** (Achievements, Help,
|
||||||
|
Stats, Profile, Leaderboard). Each modal's body Node now
|
||||||
|
carries `Overflow::scroll_y()` plus a `max_height` constraint
|
||||||
|
(`Val::Vh(70.0)` for most, `Val::Vh(50.0)` for the
|
||||||
|
leaderboard's variable-length ranking section) and a marker
|
||||||
|
component (`AchievementsScrollable`, `HelpScrollable`,
|
||||||
|
`StatsScrollable`, `ProfileScrollable`,
|
||||||
|
`LeaderboardScrollable`). A sibling `scroll_*_panel` system
|
||||||
|
per modal routes `MouseWheel` events into the body's
|
||||||
|
`ScrollPosition`. Mirrors the existing `SettingsPanelScrollable`
|
||||||
|
pattern. Home modal intentionally not scrolled — its five
|
||||||
|
mode cards + Cancel are sized to fit at 800×600 by design.
|
||||||
|
- **Modal focus arrives on the same frame the modal opens.**
|
||||||
|
Previously `attach_focusable_to_modal_buttons` and
|
||||||
|
`auto_focus_on_modal_open` ran in `Update` alongside arbitrary
|
||||||
|
click-handlers that spawn modals; with no ordering edge,
|
||||||
|
Bevy's deferred `Commands` queued the new entities but the
|
||||||
|
attach system couldn't see them on the same tick. Both systems
|
||||||
|
moved to `PostUpdate` so the schedule boundary itself supplies
|
||||||
|
the sync point — `FocusedButton` is always populated before
|
||||||
|
`app.update()` returns. The very next Tab/Enter press lands on
|
||||||
|
a populated resource instead of wasting itself moving focus
|
||||||
|
from None to the primary.
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
|
||||||
|
- 1196 passing tests (was 1178 at v0.15.0 close).
|
||||||
|
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||||
|
|
||||||
|
## [0.15.0] — 2026-05-02
|
||||||
|
|
||||||
|
In-engine replay playback, the Klondike solver + "Winnable deals
|
||||||
|
only" toggle, a 19th achievement, rolling replay history, and a
|
||||||
|
significant build-time / binary-size win from disabling Bevy's
|
||||||
|
default audio stack.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **In-engine replay playback** for the Stats overlay's Watch Replay
|
||||||
|
button. New `ReplayPlaybackPlugin` runs a state machine
|
||||||
|
(Inactive / Playing / Completed) that resets the live game to the
|
||||||
|
recorded deal and ticks through `replay.moves` at
|
||||||
|
`REPLAY_MOVE_INTERVAL_SECS` (0.45 s) firing the canonical
|
||||||
|
`MoveRequestEvent` / `DrawRequestEvent` per recorded move.
|
||||||
|
Recording is suppressed during playback so replays don't re-record
|
||||||
|
themselves.
|
||||||
|
- **Replay overlay banner** (`ReplayOverlayPlugin`) anchored to the
|
||||||
|
top of the window during playback. Shows "Replay" label, "Move N
|
||||||
|
of M" progress, and a Stop button. Z-order leaves modals
|
||||||
|
(Settings, Pause, Help) free to render on top so the player can
|
||||||
|
adjust audio mid-replay.
|
||||||
|
- **Rolling replay history** at `<data_dir>/replays.json` capped at
|
||||||
|
8 entries. Replaces the single-slot `latest_replay.json` (legacy
|
||||||
|
file is migrated forward on first launch via
|
||||||
|
`migrate_legacy_latest_replay`). Stats overlay gains a Prev / Next
|
||||||
|
selector and a "Replay N / M" caption so the player can revisit
|
||||||
|
older wins.
|
||||||
|
- **"Cinephile" achievement** (#19). Unlocks the first time
|
||||||
|
`ReplayPlaybackState` transitions Playing → Completed (i.e. the
|
||||||
|
replay played out to its end without the player pressing Stop).
|
||||||
|
Stop transitions Playing → Inactive directly so it doesn't count.
|
||||||
|
- **Klondike solver** in `solitaire_core::solver`. Iterative-DFS
|
||||||
|
with memoisation on a 64-bit canonical state hash, two budget
|
||||||
|
knobs (move_budget + state_budget) for pathological cases, and a
|
||||||
|
three-state `SolverResult` (Winnable / Unwinnable / Inconclusive).
|
||||||
|
Median solve time 2 ms; pathological inconclusives cap near
|
||||||
|
120 ms. Pure logic — `solitaire_core` keeps no Bevy or I/O.
|
||||||
|
- **"Winnable deals only" toggle** in Settings → Gameplay (default
|
||||||
|
off). When on, `handle_new_game` walks seed N, N+1, N+2, …
|
||||||
|
through `try_solve` until it finds Winnable or Inconclusive,
|
||||||
|
capped at `SOLVER_DEAL_RETRY_CAP` (50) attempts. Daily
|
||||||
|
challenges, replays, and explicit-seed requests bypass the
|
||||||
|
solver — only random Classic deals are gated.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Bevy default-feature trim** (`bevy = { default-features = false,
|
||||||
|
features = [...] }` in workspace Cargo.toml) drops 51 transitive
|
||||||
|
crates including the `bevy_audio` → rodio → cpal 0.15 + symphonia
|
||||||
|
chain that the project doesn't use (kira handles audio directly).
|
||||||
|
The retained feature list is curated to exactly what the engine
|
||||||
|
uses; `solitaire_wasm` is unaffected because it doesn't depend on
|
||||||
|
bevy.
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
|
||||||
|
- 1178 passing tests (was 1134 at v0.14.0 close).
|
||||||
|
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||||
|
|
||||||
|
## [0.14.0] — 2026-05-02
|
||||||
|
|
||||||
|
Two threads land in v0.14.0: the second half of the post-v0.12.0 UX
|
||||||
|
candidate list (theme thumbnails, daily-challenge calendar, Time Attack
|
||||||
|
auto-save, per-mode bests, time-bonus multiplier) plus a **major new
|
||||||
|
feature** — the replay pipeline (record → upload → web viewer). Three
|
||||||
|
Quat-reported bugs from a smoke-test round shipped alongside.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Theme-picker thumbnails** in Settings → Cosmetic. Each theme chip
|
||||||
|
renders a small Ace-of-Spades + back preview pair via the existing
|
||||||
|
`rasterize_svg` path. Cached per theme in a new
|
||||||
|
`ThemeThumbnailCache`. Themes that lack a preview SVG fall back to
|
||||||
|
a transparent placeholder rather than crashing.
|
||||||
|
- **14-day daily-challenge calendar** in the Profile modal. Horizontal
|
||||||
|
row of dots showing the trailing two weeks; today's dot is ringed
|
||||||
|
in `ACCENT_PRIMARY`, completed days fill `STATE_SUCCESS`, missed
|
||||||
|
days fill `BG_ELEVATED`. Caption above the row reads "Current
|
||||||
|
streak: N · Longest: M".
|
||||||
|
- **Time Attack session auto-save** to `<data_dir>/time_attack_session.json`,
|
||||||
|
atomic .tmp + rename. 30-second auto-save while a session is active,
|
||||||
|
plus on `AppExit`. Sessions whose 10-minute window expired in real
|
||||||
|
time while the app was closed are discarded on load. Classic, Zen,
|
||||||
|
and Challenge already auto-saved correctly via `game_state.json` —
|
||||||
|
Time Attack was the only mode missing session-level persistence.
|
||||||
|
- **Per-mode best-score and fastest-win readouts** in the Stats screen.
|
||||||
|
`StatsSnapshot` gains six `#[serde(default)]` fields (Classic / Zen
|
||||||
|
/ Challenge × best_score + fastest_win_seconds). Stats screen renders
|
||||||
|
a "Per-mode bests" section between the primary cell grid and
|
||||||
|
progression. Lifetime totals continue to roll all modes together.
|
||||||
|
- **Time-bonus multiplier slider** in Settings → Gameplay (0.0–2.0,
|
||||||
|
0.1 steps, default 1.0, "Off" label at zero). Cosmetic only —
|
||||||
|
multiplies the time-bonus shown in the win modal but does NOT
|
||||||
|
affect achievement unlock thresholds (those still use the raw
|
||||||
|
unmultiplied score).
|
||||||
|
- **Win-replay recording + storage.** Every move during a successful
|
||||||
|
game appends to a `RecordingReplay` resource; on `GameWonEvent`
|
||||||
|
the recording freezes into a `Replay` (seed + draw_mode + mode +
|
||||||
|
score + time + ordered move list) and persists to
|
||||||
|
`<data_dir>/latest_replay.json` atomically. Single-slot — overwrites
|
||||||
|
on every win.
|
||||||
|
- **"Watch replay" button** in the Stats overlay. Shows the latest
|
||||||
|
win's caption and surfaces a button that loads the replay (button
|
||||||
|
fires an `InfoToastEvent` describing the replay; full in-engine
|
||||||
|
playback is deferred to a future build).
|
||||||
|
- **Replay upload + fetch endpoints** on the server. `POST /api/replays`
|
||||||
|
accepts a `Replay` JSON; `GET /api/replays/:id` returns it. JWT-gated
|
||||||
|
with the existing auth middleware. Engine uploads winning replays
|
||||||
|
automatically when the player has cloud sync configured.
|
||||||
|
- **`solitaire_wasm` crate** — new workspace member compiling
|
||||||
|
replay-relevant `solitaire_core` types to WebAssembly so a
|
||||||
|
browser can re-execute a replay client-side. No-std-friendly
|
||||||
|
surface; `wasm-bindgen` glue.
|
||||||
|
- **Web replay viewer** served from the Solitaire server.
|
||||||
|
`GET /replays/:id` returns HTML + CSS + the wasm bundle that
|
||||||
|
fetches the replay JSON, rasterises a deal from the seed, and
|
||||||
|
animates the recorded moves.
|
||||||
|
- **Card flight animations on the web side** so the browser viewer
|
||||||
|
reads as a real game replay rather than a static dump.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Multi-card lift validation.** `solitaire_core::rules::is_valid_tableau_sequence`
|
||||||
|
rejects a moved stack whose adjacent cards don't form a descending
|
||||||
|
alternating-colour run. Previously a player could lift any
|
||||||
|
multi-card selection and drop it as long as the bottom landed
|
||||||
|
legally. Wired into `move_cards`'s tableau-destination branch.
|
||||||
|
- **Softlock detection.** `has_legal_moves` rewritten to walk every
|
||||||
|
potential move source (every stock card, every waste card, the
|
||||||
|
face-up top of every tableau column) and check it against every
|
||||||
|
foundation and every tableau. Previously the heuristic
|
||||||
|
early-returned `true` whenever stock had cards — players got
|
||||||
|
stuck in unwinnable end-states with no end-game screen.
|
||||||
|
`GameOverScreen` now actually fires for true softlocks. Quat's
|
||||||
|
exact reproduction case is pinned by a new test.
|
||||||
|
- **Deal-tween information leak.** New-game now snaps every card
|
||||||
|
sprite to the stock pile position before writing
|
||||||
|
`StateChangedEvent`, so all 52 cards animate from a single point
|
||||||
|
during the deal. Previously the sprites started from their
|
||||||
|
previous-game positions, briefly revealing the prior deal.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- `SESSION_HANDOFF.md` refreshed for the Quat smoke-test round
|
||||||
|
including investigation findings on solver decisions and
|
||||||
|
dependency duplicates.
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
|
||||||
|
- 1134 passing tests (was 1053 at v0.13.0 close).
|
||||||
|
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||||
|
|
||||||
|
## [0.13.0] — 2026-05-02
|
||||||
|
|
||||||
|
Third UX iteration round on top of v0.12.0. Six handoff candidates
|
||||||
|
shipped — three small polish items, three larger interaction
|
||||||
|
features (theme-aware backs, full keyboard play, right-click power
|
||||||
|
shortcut). Plus two code-review fixes (font handling unified,
|
||||||
|
sccache wiring removed).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Tooltip-delay slider** in Settings → Gameplay. `tooltip_delay_secs`
|
||||||
|
ranges [0.0, 1.5] in 0.1 s steps; "Instant" label when zero.
|
||||||
|
`Settings.tooltip_delay_secs` round-trips through serialise/deserialise
|
||||||
|
with `#[serde(default)]`. The hover-delay comparison in
|
||||||
|
`ui_tooltip` reads from `SettingsResource` with the existing
|
||||||
|
`MOTION_TOOLTIP_DELAY_SECS` as the test-fixture fallback.
|
||||||
|
- **Win-streak fire animation.** New `WinStreakMilestoneEvent` fires
|
||||||
|
from `stats_plugin` when `win_streak_current` crosses any of
|
||||||
|
[3, 5, 10] (only the threshold crossing — not every subsequent
|
||||||
|
win). The HUD streak readout scale-pulses 1.0 → 1.20 → 1.0 over
|
||||||
|
`MOTION_STREAK_FLOURISH_SECS` (0.6 s).
|
||||||
|
- **Score-breakdown reveal on the win modal.** Replaces the single
|
||||||
|
"Score: N" line with a per-component reveal (Base / Time bonus /
|
||||||
|
No-undo bonus / Mode multiplier / Total). Rows fade in over
|
||||||
|
`MOTION_SCORE_BREAKDOWN_FADE_SECS` (0.12 s) staggered by
|
||||||
|
`MOTION_SCORE_BREAKDOWN_STAGGER_SECS` (0.15 s). Honours
|
||||||
|
`AnimSpeed::Instant` by spawning all rows fully visible.
|
||||||
|
- **Card backs follow the active theme.** `theme.ron`'s `back` slot
|
||||||
|
now actually drives the face-down sprite. Active-theme back
|
||||||
|
rasterises alongside the faces and supersedes the legacy
|
||||||
|
`back_N.png` picker. The picker remains as a fallback for themes
|
||||||
|
that don't ship a back, and the Settings UI surfaces a caption
|
||||||
|
("Active theme provides its own back") + dimmed swatches when
|
||||||
|
the override is in effect.
|
||||||
|
- **Keyboard-only drag-and-drop.** Tab cycles draggable card stacks,
|
||||||
|
Enter "lifts" the focused stack, arrow keys (or Tab) cycle the
|
||||||
|
legal-destination targets only, Enter confirms, Esc cancels. A
|
||||||
|
new `KeyboardDragState` resource models the two-mode flow without
|
||||||
|
changing the existing `SelectionState` contract. Mutual exclusion
|
||||||
|
with mouse drag uses a sentinel `DragState.active_touch_id =
|
||||||
|
KEYBOARD_DRAG_TOUCH_ID` (u64::MAX) so neither pipeline can
|
||||||
|
trample the other.
|
||||||
|
- **Right-click radial menu.** Hold right-click on a face-up card →
|
||||||
|
a small ring of icons appears at the cursor with one entry per
|
||||||
|
legal destination. Release over an icon → fires
|
||||||
|
`MoveRequestEvent`; release in dead space, Esc, or left-click
|
||||||
|
cancels. Skips the drag motion entirely. New `RadialMenuPlugin`
|
||||||
|
owns the flow; co-exists with the existing `RightClickHighlight`
|
||||||
|
pile-marker tint.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Font handling consolidated to bundled-only.** Code-review
|
||||||
|
feedback: the SVG rasteriser previously mixed
|
||||||
|
`load_system_fonts` + bundled FiraMono + a lenient resolver,
|
||||||
|
which made card text rendering depend on host fontconfig. Picked
|
||||||
|
option (a) and applied it across both layers — `font_plugin` now
|
||||||
|
embeds `assets/fonts/main.ttf` via `include_bytes!()` and
|
||||||
|
registers it with `Assets<Font>`; `svg_loader::shared_fontdb`
|
||||||
|
loads only the bundled bytes; the new `bundled_font_resolver`
|
||||||
|
ignores the SVG's `font-family` request and always returns the
|
||||||
|
single bundled face. A parse failure aborts with a clear error
|
||||||
|
("bundled FiraMono failed to parse — binary is corrupt").
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- **Project-level sccache wiring.** Code-review feedback: sccache
|
||||||
|
shouldn't be a per-project build dependency. Cargo's incremental
|
||||||
|
cache already covers the single-project case, and forcing
|
||||||
|
`rustc-wrapper = "sccache"` workspace-wide meant every contributor
|
||||||
|
had to install it. `.cargo/config.toml` deleted entirely; plain
|
||||||
|
`cargo build` now works without setup.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- `help_plugin` controls reference gains a "Mouse" section covering
|
||||||
|
double-click auto-move, right-click highlight, and the new
|
||||||
|
hold-RMB radial.
|
||||||
|
- `help_plugin` also gains a "Keyboard drag" section for the new
|
||||||
|
Tab/Enter/Arrows/Esc flow.
|
||||||
|
- Onboarding slide 3 picks up a `Tab → Enter` row referencing the
|
||||||
|
full keyboard drag path.
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
|
||||||
|
- 1053 passing tests (was 1031 at v0.12.0 close).
|
||||||
|
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||||
|
|
||||||
|
## [0.12.0] — 2026-05-02
|
||||||
|
|
||||||
|
UX feel polish round on top of v0.11.0. Six small-but-tangible
|
||||||
|
improvements that make the play surface feel more responsive,
|
||||||
|
forgiving, and discoverable, plus the doc refresh that should have
|
||||||
|
ridden along with v0.11.0.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Foundation completion flourish.** When a King lands on a
|
||||||
|
foundation (Ace-through-King for that suit), a brief celebration
|
||||||
|
fires: King card scale-pulses 1.0 → 1.15 → 1.0 over 0.4 s, the
|
||||||
|
foundation marker tints `STATE_SUCCESS` for the first half then
|
||||||
|
fades, and a synthesised C6→E6→G6 bell ping plays (~240 ms,
|
||||||
|
octave above `win_fanfare`'s root so the fourth completion + win
|
||||||
|
cascade layer cleanly). New `FoundationCompletedEvent { slot,
|
||||||
|
suit }` carries the trigger so future systems can hook in.
|
||||||
|
- **Drag-cancel return tween.** Illegal drops glide each dragged
|
||||||
|
card back to its origin slot over 150 ms with a quintic ease-out
|
||||||
|
curve (`MotionCurve::Responsive`, zero overshoot — reads forgiving
|
||||||
|
rather than jittery). The audio cue (`card_invalid.wav`) still
|
||||||
|
fires for negative feedback. Right-click and double-click invalid
|
||||||
|
paths still use `ShakeAnim` since there's no motion to interpolate.
|
||||||
|
- **Focus ring breathing.** The keyboard focus ring's alpha modulates
|
||||||
|
with a 1.4 s sin curve over [0.65, 1.0] of its native value so the
|
||||||
|
indicator catches the eye on focus changes without competing with
|
||||||
|
gameplay. Honours `AnimSpeed::Instant` by reverting to the static
|
||||||
|
outline for reduced-motion users.
|
||||||
|
- **First-win achievement onboarding toast.** After the player's
|
||||||
|
very first win, a one-shot info toast surfaces "First win! Press
|
||||||
|
A to see your achievements." `Settings.shown_achievement_onboarding`
|
||||||
|
persists the seen state so the cue never re-fires (legacy
|
||||||
|
`settings.json` files load to `false` via `#[serde(default)]`).
|
||||||
|
- **Mode Launcher digit shortcuts.** Pressing M opens the Home modal
|
||||||
|
(the Mode Launcher); inside it, pressing 1–5 launches each mode
|
||||||
|
directly without needing Tab + Enter. Locked modes (Zen, Challenge,
|
||||||
|
Time Attack at level < 5) are silent no-ops. Modal-scoped — digit
|
||||||
|
keys outside the launcher fire nothing.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Card aspect ratio matches hayeah SVGs.** `CARD_ASPECT` 1.4 →
|
||||||
|
1.4523 to match the bundled artwork's natural 167.087 × 242.667
|
||||||
|
dimensions. Cards previously rendered ~3.6 % vertically squashed.
|
||||||
|
The vertical-budget math in `compute_layout` uses `CARD_ASPECT`
|
||||||
|
algebraically so the worst-case-tableau-fits-on-screen guarantee
|
||||||
|
adapts automatically.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- **README refresh** with v0.11.0+ features (card themes, HUD
|
||||||
|
overhaul, drag feel, unlocked foundations) and a corrected controls
|
||||||
|
table — the previous table inverted Z/U for undo and listed H for
|
||||||
|
help when F1 is the binding.
|
||||||
|
- **CHANGELOG.md** added (this file), covering v0.9.0–v0.12.0 with
|
||||||
|
Keep a Changelog 1.1.0 conventions.
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
|
||||||
|
- 1007 passing tests (was 982 at v0.11.0).
|
||||||
|
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||||
|
|
||||||
|
## [0.11.0] — 2026-05-02
|
||||||
|
|
||||||
|
The biggest release since 0.10.0. Headline threads: a runtime card-theme
|
||||||
|
system, an HUD restructure that reclaims the play surface, and a round of
|
||||||
|
UX feel polish surfaced by smoke testing.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Runtime card-theme system** (CARD_PLAN phases 1–7).
|
||||||
|
- Bundled default theme ships in the binary via `embedded://` — 52
|
||||||
|
[hayeah/playing-cards-assets](https://github.com/hayeah/playing-cards-assets)
|
||||||
|
SVGs (MIT) plus a midnight-purple `back.svg` as original work.
|
||||||
|
- User themes live under `themes://` rooted at `user_theme_dir()`. Drop
|
||||||
|
a directory containing `theme.ron` + 53 SVGs and the registry picks
|
||||||
|
it up on next launch.
|
||||||
|
- Importer at `solitaire_engine::theme::import_theme(zip)` validates
|
||||||
|
archives (20 MB cap, zip-slip rejection, manifest validation, every
|
||||||
|
SVG round-tripped through the rasteriser) and atomically unpacks.
|
||||||
|
- Picker UI in **Settings → Cosmetic**; selection persists as
|
||||||
|
`selected_theme_id` and propagates to live sprites.
|
||||||
|
- **Reserved HUD top band** (64 px) so cards no longer crowd the score
|
||||||
|
readout or action buttons; layout's `top_y` shifts down accordingly.
|
||||||
|
- **Action-bar auto-fade** — buttons fade out when the cursor leaves the
|
||||||
|
band, fade back in when it returns. Lerp at ~167 ms.
|
||||||
|
- **Visible drop-target overlay during drag** — a soft fill plus 3 px
|
||||||
|
outline drawn ABOVE stacked cards for every legal target (full fanned
|
||||||
|
column for tableaux, card-sized for foundations and empty tableaux).
|
||||||
|
Replaces the previously invisible pile-marker tint.
|
||||||
|
- **Card drop shadows** — every card casts a neutral 25 % black shadow
|
||||||
|
with a 4 px halo; cards in the active drag set switch to a lifted
|
||||||
|
shadow (40 % alpha, larger offset, bigger halo).
|
||||||
|
- **Stock remaining-count badge** — small `·N` chip at the top-right of
|
||||||
|
the stock pile so the player can see how close they are to a recycle.
|
||||||
|
Hides when the stock empties.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Foundations are unlocked.** `PileType::Foundation(Suit)` →
|
||||||
|
`Foundation(u8)` (slot 0..3). The claimed suit is derived from the
|
||||||
|
bottom card via `Pile::claimed_suit()` — no separate field, no
|
||||||
|
claim-stuck-after-undo bugs. Any Ace lands in any empty slot, and the
|
||||||
|
slot then claims that suit. `next_auto_complete_move` prefers a
|
||||||
|
claim-matched slot before falling back to the first empty slot for
|
||||||
|
Aces. Empty foundation markers render as plain placeholders (no
|
||||||
|
"C/D/H/S").
|
||||||
|
- **HUD selection label** and **hint toast** read `claimed_suit()` and
|
||||||
|
fall through to "Foundation N" / "move to foundation" only when the
|
||||||
|
slot is empty.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`shared_fontdb` now bundles FiraMono.** The hayeah SVGs reference
|
||||||
|
`Bitstream Vera Sans` and `Arial` by name. On minimal Linux installs
|
||||||
|
/ fresh Wayland sessions / chroots where neither is installed AND the
|
||||||
|
CSS-generic aliases don't resolve, card rank/suit text vanished. The
|
||||||
|
bundled font is loaded into fontdb and pinned as every CSS generic's
|
||||||
|
target so the resolver always lands on something real. Surfaced when
|
||||||
|
a second-machine pull rendered cards without glyphs.
|
||||||
|
- **Theme asset path resolution** — `AssetPath::resolve` (concatenates)
|
||||||
|
→ `resolve_embed` (RFC 1808 sibling resolution). Was producing paths
|
||||||
|
like `…/theme.ron/hearts_4.svg` and failing to load every face SVG.
|
||||||
|
- **Sync exit log spam** — `push_on_exit` silently no-ops on
|
||||||
|
`LocalOnlyProvider`'s `UnsupportedPlatform` instead of warn-spamming
|
||||||
|
every shutdown.
|
||||||
|
- **usvg font-substitution warn spam** — custom `FontResolver.select_font`
|
||||||
|
appends `Family::SansSerif` and `Family::Serif` to every query so
|
||||||
|
unmatched named families silently fall through.
|
||||||
|
|
||||||
|
### Migration
|
||||||
|
|
||||||
|
- **In-progress saves invalidated.** `GameState.schema_version` bumped
|
||||||
|
1 → 2; pre-v2 `game_state.json` files silently fall through to "fresh
|
||||||
|
game on launch." Stats, progress, achievements, and settings live in
|
||||||
|
separate files and are unaffected.
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
|
||||||
|
- 982 passing tests (was 819 at v0.10.0).
|
||||||
|
- Zero clippy warnings under `--workspace --all-targets -- -D warnings`.
|
||||||
|
|
||||||
|
## [0.10.0] — 2026-04-29
|
||||||
|
|
||||||
|
PNG art pipeline plus a major dependency pass. The first release where
|
||||||
|
the binary shipped with bundled artwork.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **52 individual card face PNGs** generated via `solitaire_assetgen`.
|
||||||
|
- **Custom font** (FiraMono-Medium) loaded via `AssetServer` at startup
|
||||||
|
through the new `FontPlugin`.
|
||||||
|
- **Card backs and backgrounds** upgraded to 120×168 with richer
|
||||||
|
patterns.
|
||||||
|
- **Ambient audio loop** wired through the kira mixer.
|
||||||
|
- **Arch Linux PKGBUILDs** for the game client and sync server (under
|
||||||
|
the separate `solitaire-quest-pkgbuild` directory).
|
||||||
|
- **Workspace README, CI workflow, migration guide.**
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **Bevy 0.15 → 0.18** workspace migration.
|
||||||
|
- **kira 0.9 → 0.12** audio backend migration.
|
||||||
|
- **Edition 2024**, MSRV pinned to **Rust 1.95**.
|
||||||
|
- **rand 0.9** upgrade.
|
||||||
|
- **Card rendering** moved from `Text2d` overlay to PNG-backed
|
||||||
|
`Sprite` with face/back atlases; `Text2d` retained as a headless
|
||||||
|
fallback when `CardImageSet` is absent (tests under MinimalPlugins).
|
||||||
|
- **Asset pipeline** switched from `include_bytes!()` for PNGs/TTFs to
|
||||||
|
runtime `AssetServer::load()` so artwork can be swapped without a
|
||||||
|
recompile. Audio remains embedded.
|
||||||
|
- **Removed Google Play Games Services sync backend** — redundant with
|
||||||
|
the self-hosted server.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Server JWT secret** loaded at startup (was lazy, surfaced as
|
||||||
|
intermittent 500s).
|
||||||
|
- **Daily-challenge race** in the server's seed-generation path.
|
||||||
|
- **Rate limiter** switched to `SmartIpKeyExtractor` so the limit
|
||||||
|
applies per real client IP rather than per upstream proxy.
|
||||||
|
- **Touch input** uses `MessageReader<TouchInput>` (Bevy 0.18 rename).
|
||||||
|
- **Sync push/pull races** in async task scheduling.
|
||||||
|
- **Hot-path allocations** reduced in card-rendering systems.
|
||||||
|
- **Conflict report coverage** added for sync merge edge cases.
|
||||||
|
|
||||||
|
### Stats
|
||||||
|
|
||||||
|
- 819 passing tests at tag time.
|
||||||
|
|
||||||
|
## [0.9.0] — 2026-04-28
|
||||||
|
|
||||||
|
Initial public-tagged release. Established the workspace structure
|
||||||
|
(`solitaire_core` / `_sync` / `_data` / `_engine` / `_server` / `_app` /
|
||||||
|
`_assetgen`), the modal scaffold via `ui_modal`, the design-token system
|
||||||
|
in `ui_theme`, and the four-tier HUD layout. Foundations were
|
||||||
|
suit-locked at this point; cards rendered as `Text2d` rank/suit overlays
|
||||||
|
with no PNG artwork yet.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Klondike core (Draw One / Draw Three modes).
|
||||||
|
- Progression system (XP, levels, 18 achievements, daily challenge,
|
||||||
|
weekly goals, special modes at level 5).
|
||||||
|
- Self-hosted sync server (Axum + SQLite + JWT auth).
|
||||||
|
- All 12 overlay screens migrated to the `ui_modal` scaffold with real
|
||||||
|
Primary/Secondary/Tertiary buttons.
|
||||||
|
- Animation upgrades: `SmoothSnap` slide curves, scoped settle bounce,
|
||||||
|
deal jitter, win-cascade rotation.
|
||||||
|
- Splash screen, focus rings (Phases 1–3), tooltips infrastructure +
|
||||||
|
HUD/Settings/popover applications, achievement integration tests,
|
||||||
|
destructive-confirm verb unification, leaderboard error/idle states,
|
||||||
|
first-launch empty-state polish, hit-target accessibility fix,
|
||||||
|
CREDITS.md, persistent window geometry, mode-launcher Home repurpose,
|
||||||
|
client-side sync round-trip integration tests.
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/funman300/Rusty_Solitaire/compare/v0.16.0...HEAD
|
||||||
|
[0.16.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.15.0...v0.16.0
|
||||||
|
[0.15.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.14.0...v0.15.0
|
||||||
|
[0.14.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.13.0...v0.14.0
|
||||||
|
[0.13.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.12.0...v0.13.0
|
||||||
|
[0.12.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.11.0...v0.12.0
|
||||||
|
[0.11.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.10.0...v0.11.0
|
||||||
|
[0.10.0]: https://github.com/funman300/Rusty_Solitaire/compare/v0.9.0...v0.10.0
|
||||||
|
[0.9.0]: https://github.com/funman300/Rusty_Solitaire/releases/tag/v0.9.0
|
||||||
@@ -47,7 +47,9 @@ cargo clippy -p solitaire_core -- -D warnings
|
|||||||
|
|
||||||
- `solitaire_core` and `solitaire_sync` must never gain Bevy or network dependencies.
|
- `solitaire_core` and `solitaire_sync` must never gain Bevy or network dependencies.
|
||||||
- No `unwrap()` or `panic!()` in game logic. All state transitions return `Result<_, MoveError>`.
|
- No `unwrap()` or `panic!()` in game logic. All state transitions return `Result<_, MoveError>`.
|
||||||
- Audio assets are embedded at compile time using `include_bytes!()` in `audio_plugin.rs`. Cards and backgrounds are rendered procedurally (colored `Sprite` entities + text) — no image files are used and no `AssetServer` is needed.
|
- Audio assets are embedded at compile time using `include_bytes!()` in `audio_plugin.rs`.
|
||||||
|
- Card faces (52 PNGs in `assets/cards/faces/`), card backs (`assets/cards/backs/back_N.png`), board backgrounds (`assets/backgrounds/bg_N.png`), and the UI font (`assets/fonts/main.ttf`) are loaded at runtime via `AssetServer::load()` and stored as `Handle<Image>`/`Handle<Font>` in the `CardImageSet`, `BackgroundImageSet`, and `FontResource` resources. The `assets/` directory must ship alongside the binary.
|
||||||
|
- Asset-loading systems take `Option<Res<AssetServer>>` so they degrade cleanly under `MinimalPlugins` (tests). When `CardImageSet` is absent, `card_plugin` falls back to a `Text2d` rank+suit overlay; when `BackgroundImageSet` is absent, the board falls back to a solid colour.
|
||||||
- Atomic file writes only: write to `filename.json.tmp`, then `rename()`.
|
- Atomic file writes only: write to `filename.json.tmp`, then `rename()`.
|
||||||
- Passwords and tokens are stored in the OS keychain via the `keyring` crate — never in plaintext files or logs.
|
- Passwords and tokens are stored in the OS keychain via the `keyring` crate — never in plaintext files or logs.
|
||||||
- Sync runs on `AsyncComputeTaskPool` — never block the Bevy main thread.
|
- Sync runs on `AsyncComputeTaskPool` — never block the Bevy main thread.
|
||||||
@@ -75,6 +77,7 @@ cargo clippy -p solitaire_core -- -D warnings
|
|||||||
- Resources own shared state. Events communicate between systems. Components own per-entity data.
|
- Resources own shared state. Events communicate between systems. Components own per-entity data.
|
||||||
- All UI screens are built with Bevy UI (`bevy::ui`). Never mix UI layout and game logic in the same system.
|
- All UI screens are built with Bevy UI (`bevy::ui`). Never mix UI layout and game logic in the same system.
|
||||||
- Layout is recomputed on `WindowResized` — never assume a fixed window size.
|
- Layout is recomputed on `WindowResized` — never assume a fixed window size.
|
||||||
|
- **UI-first.** Every player-triggered action (new game, undo, draw, pause, open stats / settings / help / profile / leaderboard, switch mode, etc.) must be reachable from a visible UI control. Keyboard shortcuts are optional accelerators — never the sole entry point. New gameplay features ship with the UI control alongside the system that backs it; do not merge a feature that is keyboard-only.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Credits
|
||||||
|
|
||||||
|
Solitaire Quest is MIT-licensed (see [LICENSE](LICENSE)). It is built on top of
|
||||||
|
the work of many open-source projects and a small handful of third-party
|
||||||
|
assets. This file lists every component that ships in the binary or in the
|
||||||
|
`assets/` directory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code & Framework
|
||||||
|
|
||||||
|
| Component | License | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| [Bevy 0.18](https://bevyengine.org/) | MIT OR Apache-2.0 | Game engine, ECS, rendering, UI |
|
||||||
|
| [kira 0.12](https://crates.io/crates/kira) | MIT OR Apache-2.0 | Audio playback (mixer, sub-tracks, looping ambient) |
|
||||||
|
| [serde](https://crates.io/crates/serde) / [serde_json](https://crates.io/crates/serde_json) | MIT OR Apache-2.0 | Serialization for save files and the sync API |
|
||||||
|
| [tokio](https://crates.io/crates/tokio) | MIT | Async runtime for the sync client and server |
|
||||||
|
| [axum 0.8](https://crates.io/crates/axum) | MIT | HTTP framework for the self-hosted sync server |
|
||||||
|
| [sqlx 0.8](https://crates.io/crates/sqlx) | MIT OR Apache-2.0 | Compile-time-checked SQLite access on the server |
|
||||||
|
| [reqwest 0.13](https://crates.io/crates/reqwest) | MIT OR Apache-2.0 | HTTP client for the sync provider |
|
||||||
|
| [jsonwebtoken 10](https://crates.io/crates/jsonwebtoken) | MIT | JWT issuance and validation |
|
||||||
|
| [bcrypt 0.19](https://crates.io/crates/bcrypt) | MIT | Password hashing on the server |
|
||||||
|
| [keyring 4](https://crates.io/crates/keyring) | MIT OR Apache-2.0 | OS keychain integration for credential storage |
|
||||||
|
| [tower-governor 0.8](https://crates.io/crates/tower-governor) | MIT | Rate limiting on `/api/auth/*` |
|
||||||
|
| [chrono](https://crates.io/crates/chrono) | MIT OR Apache-2.0 | Date / time handling |
|
||||||
|
| [uuid](https://crates.io/crates/uuid) | MIT OR Apache-2.0 | User and session identifiers |
|
||||||
|
| [thiserror](https://crates.io/crates/thiserror) | MIT OR Apache-2.0 | Error type derive |
|
||||||
|
| [rand 0.9](https://crates.io/crates/rand) | MIT OR Apache-2.0 | Seeded shuffler in `solitaire_core` |
|
||||||
|
| [png 0.17](https://crates.io/crates/png) | MIT OR Apache-2.0 | PNG encoder used by `solitaire_assetgen` |
|
||||||
|
| [ab_glyph 0.2](https://crates.io/crates/ab_glyph) | Apache-2.0 | Glyph rasterization for generated card art |
|
||||||
|
|
||||||
|
The full transitive dependency tree (several hundred crates) is captured in
|
||||||
|
`Cargo.lock` and reachable via `cargo tree`. Every crate brought in is
|
||||||
|
MIT, Apache-2.0, BSD-style, or a dual-licensed combination thereof — no
|
||||||
|
copyleft code is statically linked into the game binary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
### Card artwork
|
||||||
|
|
||||||
|
| File(s) | Source | License |
|
||||||
|
|---|---|---|
|
||||||
|
| `solitaire_engine/assets/themes/default/{suit}_{rank}.svg` (52 SVGs) | [hayeah/playing-cards-assets](https://github.com/hayeah/playing-cards-assets) | MIT |
|
||||||
|
| `solitaire_engine/assets/themes/default/back.svg` | Original — Solitaire Quest | MIT (this project) |
|
||||||
|
| `assets/cards/faces/{RANK}{SUIT}.png` (52 PNGs) | Pre-rendered from the same `playing-cards-assets` SVGs | MIT (passed through from hayeah) |
|
||||||
|
| `assets/cards/backs/back_0.png` – `back_4.png` | Original — generated by `solitaire_assetgen::gen_art` | MIT (this project) |
|
||||||
|
|
||||||
|
The face SVGs come from Howard Yeh's `playing-cards-assets` repository, which
|
||||||
|
is itself derived from the public-domain `vector-playing-cards` Google Code
|
||||||
|
project. The art is redistributed under the MIT license — see the upstream
|
||||||
|
repository for the full notice. The files ship unmodified in the bundled
|
||||||
|
default theme; user-supplied themes can override them per-installation
|
||||||
|
through the runtime SVG theming system documented in `CARD_PLAN.md`.
|
||||||
|
|
||||||
|
The default card back is original work by this project, midnight-purple
|
||||||
|
themed to match the rest of the UI palette.
|
||||||
|
|
||||||
|
### Backgrounds
|
||||||
|
|
||||||
|
| File(s) | Source | License |
|
||||||
|
|---|---|---|
|
||||||
|
| `assets/backgrounds/bg_0.png` – `bg_4.png` | Original — generated by `solitaire_assetgen::gen_art` | MIT (this project) |
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
| File | Source | License |
|
||||||
|
|---|---|---|
|
||||||
|
| `assets/fonts/main.ttf` (FiraMono-Medium) | [mozilla/Fira](https://github.com/mozilla/Fira) | SIL Open Font License 1.1 |
|
||||||
|
|
||||||
|
The OFL permits redistribution and embedding in software so long as the font
|
||||||
|
file itself is not sold standalone. The file ships unmodified.
|
||||||
|
|
||||||
|
### Audio
|
||||||
|
|
||||||
|
All six WAV files in `assets/audio/` are **original work** — there are no
|
||||||
|
third-party audio samples in this project. They are synthesized
|
||||||
|
programmatically by `solitaire_assetgen/src/bin/gen_sfx.rs`, which writes
|
||||||
|
44.1 kHz mono 16-bit PCM WAVs using a hand-rolled WAV writer (no `hound` or
|
||||||
|
`dasp` dependency). The synthesis stack is entirely additive: sine /
|
||||||
|
square waves, layered harmonics, deterministic LCG noise, AR envelopes,
|
||||||
|
and a slow LFO for the ambient track.
|
||||||
|
|
||||||
|
| File | Synthesis approach |
|
||||||
|
|---|---|
|
||||||
|
| `card_deal.wav` | Filtered LCG noise with a sweeping low-pass cutoff for a "whoosh" |
|
||||||
|
| `card_flip.wav` | High-passed LCG noise under a fast AR envelope |
|
||||||
|
| `card_place.wav` | 120 Hz sine body + filtered noise click |
|
||||||
|
| `card_invalid.wav` | Two dissonant square tones (196 Hz + 207.65 Hz) beating against each other |
|
||||||
|
| `win_fanfare.wav` | C-major arpeggio (C5 / E5 / G5 / C6) with sine + 2nd harmonic |
|
||||||
|
| `ambient_loop.wav` | 55 Hz fundamental with 2nd and 3rd harmonics, modulated by a 0.2 Hz LFO; loop length is chosen so the tone and LFO both complete an integer number of cycles for seamless looping |
|
||||||
|
|
||||||
|
Audio files are MIT-licensed alongside the rest of this project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License Summary
|
||||||
|
|
||||||
|
- **Project code:** MIT — see [LICENSE](LICENSE).
|
||||||
|
- **Card face artwork (52 SVGs from hayeah/playing-cards-assets, plus the
|
||||||
|
pre-rendered PNGs in `assets/cards/faces/`):** MIT, redistributed
|
||||||
|
unmodified. The original `vector-playing-cards` line art is itself
|
||||||
|
public domain.
|
||||||
|
- **FiraMono-Medium font:** SIL Open Font License 1.1, redistributed unmodified.
|
||||||
|
- **All other assets** (backgrounds, the default `back.svg`, generated card
|
||||||
|
backs, every audio file) are original work covered by this project's MIT
|
||||||
|
license.
|
||||||
|
|
||||||
|
If you redistribute Solitaire Quest, you must ship this `CREDITS.md` and the
|
||||||
|
`LICENSE` file alongside the binary so the MIT (project + hayeah card art)
|
||||||
|
and OFL (FiraMono) notices remain visible to end users.
|
||||||
@@ -7,6 +7,7 @@ members = [
|
|||||||
"solitaire_server",
|
"solitaire_server",
|
||||||
"solitaire_app",
|
"solitaire_app",
|
||||||
"solitaire_assetgen",
|
"solitaire_assetgen",
|
||||||
|
"solitaire_wasm",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
@@ -35,9 +36,72 @@ solitaire_sync = { path = "solitaire_sync" }
|
|||||||
solitaire_data = { path = "solitaire_data" }
|
solitaire_data = { path = "solitaire_data" }
|
||||||
solitaire_engine = { path = "solitaire_engine" }
|
solitaire_engine = { path = "solitaire_engine" }
|
||||||
|
|
||||||
bevy = "0.18"
|
# Bevy with `default-features = false` to avoid the unused
|
||||||
|
# `bevy_audio → rodio + symphonia + cpal 0.15 + alsa 0.9` chain.
|
||||||
|
# Audio is handled directly by `kira` in `audio_plugin.rs`, so the
|
||||||
|
# `bevy_audio` feature is intentionally omitted. The features below
|
||||||
|
# enumerate every leaf of the standard `2d` + `ui` meta-features that
|
||||||
|
# we actually use; new features should only be added with a
|
||||||
|
# corresponding use site.
|
||||||
|
bevy = { version = "0.18", default-features = false, features = [
|
||||||
|
# default_app
|
||||||
|
"async_executor",
|
||||||
|
"bevy_asset",
|
||||||
|
"bevy_input_focus",
|
||||||
|
"bevy_log",
|
||||||
|
"bevy_state",
|
||||||
|
"bevy_window",
|
||||||
|
"custom_cursor",
|
||||||
|
"reflect_auto_register",
|
||||||
|
# default_platform (desktop subset; no android/wayland/webgl/gilrs/sysinfo)
|
||||||
|
"std",
|
||||||
|
"bevy_winit",
|
||||||
|
"default_font",
|
||||||
|
"multi_threaded",
|
||||||
|
"x11",
|
||||||
|
# common_api
|
||||||
|
"bevy_color",
|
||||||
|
"bevy_image",
|
||||||
|
"bevy_mesh",
|
||||||
|
"bevy_shader",
|
||||||
|
"bevy_text",
|
||||||
|
"png",
|
||||||
|
# 2d rendering
|
||||||
|
"bevy_camera",
|
||||||
|
"bevy_render",
|
||||||
|
"bevy_core_pipeline",
|
||||||
|
"bevy_sprite",
|
||||||
|
"bevy_sprite_render",
|
||||||
|
# UI rendering
|
||||||
|
"bevy_ui",
|
||||||
|
"bevy_ui_render",
|
||||||
|
] }
|
||||||
kira = "0.12"
|
kira = "0.12"
|
||||||
|
|
||||||
|
# SVG rasterisation pipeline for the runtime card-theme system.
|
||||||
|
# usvg parses + simplifies; resvg renders to a tiny-skia Pixmap;
|
||||||
|
# tiny-skia provides the CPU rasteriser. All three are maintained
|
||||||
|
# together by the resvg-rs project and version in lockstep.
|
||||||
|
usvg = "0.47"
|
||||||
|
resvg = "0.47"
|
||||||
|
tiny-skia = "0.12"
|
||||||
|
|
||||||
|
# Theme manifest format. RON keeps the file human-editable while
|
||||||
|
# preserving Rust-style structures the importer can validate.
|
||||||
|
ron = "0.12"
|
||||||
|
|
||||||
|
# Importer-only: reads user-supplied theme zip archives, validates
|
||||||
|
# their contents, and unpacks them into the user themes directory.
|
||||||
|
# Default features are disabled to keep the dependency footprint small;
|
||||||
|
# only `deflate` is needed because the importer rejects other
|
||||||
|
# compression methods anyway (see Phase 7 spec).
|
||||||
|
zip = { version = "8.6", default-features = false, features = ["deflate"] }
|
||||||
|
|
||||||
|
# Importer-only test dependency: tests build zip archives in a
|
||||||
|
# scratch directory so they don't pollute the real user themes path
|
||||||
|
# on the developer's machine.
|
||||||
|
tempfile = "3.27"
|
||||||
|
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||||
jsonwebtoken = { version = "10", default-features = false, features = ["rust_crypto"] }
|
jsonwebtoken = { version = "10", default-features = false, features = ["rust_crypto"] }
|
||||||
|
|||||||
@@ -1,17 +1,35 @@
|
|||||||
# Solitaire Quest
|
# Solitaire Quest
|
||||||
|
|
||||||
A cross-platform Klondike Solitaire game written in Rust, featuring a full progression system with XP, levels, achievements, daily challenges, and optional self-hosted sync so your stats follow you across machines.
|
A cross-platform Klondike Solitaire game written in Rust, with a card-theme
|
||||||
|
system, full progression (XP / levels / achievements / daily challenges), and
|
||||||
|
optional self-hosted sync so your stats follow you across machines.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Klondike Solitaire** — Draw One and Draw Three modes
|
- **Klondike Solitaire** — Draw One and Draw Three modes; foundations are
|
||||||
|
unlocked (any Ace lands in any empty slot, the slot then claims that suit)
|
||||||
|
- **Card themes** — bundled hayeah/playing-cards-assets default plus
|
||||||
|
user-installable themes (drop a directory under the data dir or import a
|
||||||
|
zip from Settings → Cosmetic)
|
||||||
|
- **Modern HUD** — reserved top band keeps cards from crowding the score
|
||||||
|
readout; the action bar auto-fades when the cursor leaves it so it can't
|
||||||
|
compete with the play surface
|
||||||
|
- **Drag feel** — every legal drop target is highlighted in green during
|
||||||
|
drag; cards cast a soft drop shadow that lifts when picked up; the stock
|
||||||
|
pile shows a remaining-count chip so you can see how close you are to a
|
||||||
|
recycle
|
||||||
|
- **Keyboard navigation** — Tab cycles focus through buttons, arrow keys
|
||||||
|
move within picker rows, Enter activates; works across every modal and
|
||||||
|
the HUD action bar
|
||||||
- **Progression** — XP, levels, unlockable card backs and backgrounds
|
- **Progression** — XP, levels, unlockable card backs and backgrounds
|
||||||
- **18 Achievements** — including secret ones
|
- **19 Achievements** — including secret ones
|
||||||
- **Daily Challenge** — server-seeded so every player worldwide gets the same deal
|
- **Daily Challenge** — server-seeded so every player worldwide gets the
|
||||||
|
same deal
|
||||||
- **Leaderboard** — opt-in, powered by your own self-hosted server
|
- **Leaderboard** — opt-in, powered by your own self-hosted server
|
||||||
- **Special Modes** (unlocked at level 5): Zen, Time Attack, Challenge
|
- **Special Modes** (unlocked at level 5): Zen, Time Attack, Challenge
|
||||||
- **Sync** — pull/push stats across devices via a self-hosted server
|
- **Sync** — pull/push stats across devices via a self-hosted server
|
||||||
- **Color-blind mode** — blue tint on red-suit cards
|
- **Color-blind mode** — blue tint on red-suit cards alongside the suit
|
||||||
|
glyph
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@@ -32,42 +50,73 @@ cargo build -p solitaire_app --release
|
|||||||
|
|
||||||
## Controls
|
## Controls
|
||||||
|
|
||||||
|
Every action also has a visible UI button — keyboard shortcuts are optional
|
||||||
|
accelerators.
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Left click / drag | Move cards |
|
| Left click / drag | Move cards |
|
||||||
|
| Double click | Auto-move card to its best legal destination |
|
||||||
| Right click | Highlight legal moves for a card |
|
| Right click | Highlight legal moves for a card |
|
||||||
| Space / D | Draw from stock |
|
| Space / D | Draw from stock |
|
||||||
| Z / Ctrl+Z | Undo |
|
| U | Undo |
|
||||||
|
| H | Hint (highlight a legal move) |
|
||||||
| N | New game |
|
| N | New game |
|
||||||
| S | Stats overlay |
|
| Z | Zen mode |
|
||||||
| A | Achievements overlay |
|
| G | Forfeit (during pause) |
|
||||||
| P | Profile overlay |
|
| Tab / Shift+Tab | Cycle keyboard focus |
|
||||||
| O | Settings |
|
| Enter | Activate focused button / auto-complete (when badge is lit) |
|
||||||
| L | Leaderboard |
|
| Esc | Pause / dismiss modal |
|
||||||
| H | Help / controls |
|
| F1 | Help / controls |
|
||||||
| Enter | Auto-complete (when badge is lit) |
|
| F11 | Toggle fullscreen |
|
||||||
| Escape | Pause / clear selection |
|
| S / A / P / O / L / M | Stats / Achievements / Profile / Settings / Leaderboard / Menu |
|
||||||
| Arrow keys | Navigate card selection |
|
|
||||||
|
## Card themes
|
||||||
|
|
||||||
|
The default theme ships embedded in the binary, so the game runs
|
||||||
|
self-contained with no external assets. To install another theme, drop a
|
||||||
|
directory containing a `theme.ron` manifest plus 53 SVG files (52 faces +
|
||||||
|
1 back) under the platform data dir's `themes/` folder, or import a zip
|
||||||
|
from **Settings → Cosmetic**. The picker chip lights up the moment a new
|
||||||
|
theme is registered. Themes are SVG-based, so they rasterise cleanly at
|
||||||
|
whatever resolution the window happens to be.
|
||||||
|
|
||||||
## Sync Server (optional)
|
## Sync Server (optional)
|
||||||
|
|
||||||
To sync stats across machines, run the self-hosted server. See [README_SERVER.md](README_SERVER.md) for setup instructions.
|
To sync stats across machines, run the self-hosted server. See
|
||||||
|
[README_SERVER.md](README_SERVER.md) for setup instructions.
|
||||||
|
|
||||||
Once the server is running, open **Settings → Sync Backend**, enter the server URL and your username, and register an account from within the game.
|
Once the server is running, open **Settings → Sync Backend**, enter the
|
||||||
|
server URL and your username, and register an account from within the
|
||||||
|
game.
|
||||||
|
|
||||||
## Running Tests
|
## Running Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# All tests
|
# All tests (982 passing as of v0.11.0)
|
||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
|
|
||||||
# Just game logic (no display required)
|
# Just game logic (no display required)
|
||||||
cargo test -p solitaire_core -p solitaire_sync -p solitaire_data -p solitaire_server
|
cargo test -p solitaire_core -p solitaire_sync -p solitaire_data -p solitaire_server
|
||||||
|
|
||||||
# Lint
|
# Lint
|
||||||
cargo clippy --workspace -- -D warnings
|
cargo clippy --workspace --all-targets -- -D warnings
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
Built on [Bevy](https://bevyengine.org/) and the wider Rust ecosystem
|
||||||
|
(Tokio, Axum, sqlx, Serde, kira, and many more). Card faces come from
|
||||||
|
[hayeah/playing-cards-assets](https://github.com/hayeah/playing-cards-assets)
|
||||||
|
(MIT, derived from the public-domain `vector-playing-cards` library); the
|
||||||
|
default card back is original work; the UI font is FiraMono-Medium (OFL).
|
||||||
|
All audio is synthesized programmatically by this project. See
|
||||||
|
[CREDITS.md](CREDITS.md) for the full list and license details.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
See [CHANGELOG.md](CHANGELOG.md).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT — see [LICENSE](LICENSE).
|
MIT — see [LICENSE](LICENSE).
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Solitaire Quest — Session Handoff
|
||||||
|
|
||||||
|
**Last updated:** 2026-05-06 (post-v0.17.0) — v0.17.0 cut on top of v0.16.0 bundling the solver-driven hints (`87275bf`) and the replay-rate slider (`53e3b81`). An async-solver attempt earlier in the session was rolled back when an agent left 3 failing tests during interruption — flagged as carryover. Test-to-work ratio noted as a quality signal: future agent briefs scale back to behaviour-level tests only, not stdlib/serde-derive coverage.
|
||||||
|
|
||||||
|
## Status at pause
|
||||||
|
|
||||||
|
- **HEAD on origin:** v0.17.0's tag commit.
|
||||||
|
- **Working tree:** clean apart from untracked `CARD_PLAN.md` (intentional).
|
||||||
|
- **Build:** `cargo clippy --workspace --all-targets -- -D warnings` clean.
|
||||||
|
- **Tests:** **1208 passed / 0 failed** across the workspace.
|
||||||
|
- **Tags on origin:** `v0.9.0` through `v0.17.0`.
|
||||||
|
|
||||||
|
## Where we are
|
||||||
|
|
||||||
|
v0.16.0 is the smallest meaningful release in a while — a focused round on how modals feel rather than what they contain. The originating bug was "I can't scroll on the Achievements list"; the sweep that followed found four other modals with the same problem plus three smaller modal-feel gaps (no pointer cursor on buttons, focus arriving a frame late, no click-outside-to-dismiss).
|
||||||
|
|
||||||
|
Every overlay screen now: scrolls if its content can overflow at 800×600, shows a hand cursor when you hover any button, has its primary auto-focused the moment the modal appears so the very first Tab/Enter is meaningful, and (for read-only screens) dismisses when you click outside the card.
|
||||||
|
|
||||||
|
The post-v0.15.0 next-round candidates are still mostly open — solver-driven hints, replay-rate slider, solver progress overlay, async solver, "won previously" indicator, replay sharing. Direction is open.
|
||||||
|
|
||||||
|
### Design direction (unchanged)
|
||||||
|
|
||||||
|
- **Tone:** Balatro — chunky readable type, theatrical hierarchy, satisfying micro-interactions.
|
||||||
|
- **Palette:** Midnight Purple base + Balatro yellow primary + warm magenta secondary.
|
||||||
|
- See `~/.claude/projects/-home-manage-Rusty-Solitare/memory/project_ux_overhaul_2026-04.md` (machine-local).
|
||||||
|
|
||||||
|
### Canonical remote
|
||||||
|
|
||||||
|
`github.com/funman300/Rusty_Solitaire` is the canonical repo. Always push there.
|
||||||
|
|
||||||
|
## v0.17.0 (shipped 2026-05-06)
|
||||||
|
|
||||||
|
| Area | Commit | What landed |
|
||||||
|
|---|---|---|
|
||||||
|
| Solver-driven hints | `87275bf` | The H-key hint asks the solver for the actual best first move via `try_solve_with_first_move` / `try_solve_from_state`. Heuristic stays as fallback. Median 2 ms per H press. |
|
||||||
|
| Replay-rate slider | `53e3b81` | Settings → Gameplay slider tunes `replay_move_interval_secs` 0.10–1.00 s in 0.05 s steps; default 0.45 s. Read per frame from `SettingsResource`. |
|
||||||
|
|
||||||
|
## v0.16.0 (shipped 2026-05-06)
|
||||||
|
|
||||||
|
| Area | Commit | What landed |
|
||||||
|
|---|---|---|
|
||||||
|
| Modal scroll | `7a3032b` | Achievements / Help / Stats / Profile / Leaderboard bodies now carry `Overflow::scroll_y()` + a `max_height` constraint + a per-plugin `*Scrollable` marker. Sibling `scroll_*_panel` systems route `MouseWheel` into the body's `ScrollPosition`. Mirrors the existing `SettingsPanelScrollable` pattern. Home modal not scrolled — five mode cards + Cancel are sized to fit by design. |
|
||||||
|
| Pointer cursor | `cd54ce1` | `update_cursor_icon` gains a fourth branch: `SystemCursorIcon::Pointer` whenever any `Interaction::Hovered`/`Pressed` button is detected and no card drag is active. Branch order Grabbing → Pointer → Grab → Default. Pure `pick_cursor_icon(is_dragging, any_button_hovered, any_card_hovered)` helper unit-tests the priority. |
|
||||||
|
| Same-frame focus | `48e4121` | `attach_focusable_to_modal_buttons` and `auto_focus_on_modal_open` moved from `Update` to `PostUpdate`. The schedule boundary supplies the sync point so a click-handler in `Update` that spawns a modal has its `Commands` materialised before attach runs. `FocusedButton` is populated before `app.update()` returns; the very first Tab/Enter after open lands on a populated resource. |
|
||||||
|
| Scrim dismiss core | `a54201e` | New `ScrimDismissible` marker on `ModalScrim` opts a modal into click-outside-to-close. `dismiss_modal_on_scrim_click` system in `ui_modal` despawns the topmost dismissible scrim on a left-mouse press whose cursor lands on the scrim and outside every `ModalCard`. Stats / Achievements / Help opted in. |
|
||||||
|
| Scrim dismiss tail | `cbf2483` | One-line opt-in (capture scrim + insert marker) for Profile / Leaderboard / Home, completing all six read-only modals. |
|
||||||
|
|
||||||
|
## Open punch list
|
||||||
|
|
||||||
|
### Release prep
|
||||||
|
|
||||||
|
1. **Desktop packaging** per `ARCHITECTURE.md §17`. Arch PKGBUILD exists in `/home/manage/solitaire-quest-pkgbuild/` (separate repo). Pending: app icon, macOS `.icns` + notarisation cert, Windows `.ico` + Authenticode cert, AppImage recipe.
|
||||||
|
|
||||||
|
### Process note (raised this session)
|
||||||
|
|
||||||
|
Recent agent briefs reflexively asked for ≥3 tests per feature, which produced low-value coverage on trivial settings fields (default-value tests, serde-derive round-trips, clamp tests that just exercise stdlib `clamp`). Future agent briefs should ask only for tests that pin **behaviour contracts or regressions on real bugs** — not coverage of language/library mechanics.
|
||||||
|
|
||||||
|
### Carryover candidates — still open
|
||||||
|
|
||||||
|
- **Solver-on-AsyncComputeTaskPool** — current solver runs synchronously on the main thread. Worst-case 50 attempts × 120 ms = 6 s of UI stall on pathological seeds. **An attempt this session was rolled back** when an agent was interrupted leaving 3 failing tests; redoing this needs more careful scoping (smaller pieces, real cancel-and-test flow, NOT a parallel agent split). Worth taking next.
|
||||||
|
- **Per-deal "won previously" indicator** — the rolling replay history's seeds make this easy: when a new game starts on a seed the player has already won, surface a tiny indicator on the HUD.
|
||||||
|
- **Replay sharing** — `replays.json` is per-machine. Allow a player to copy a replay's URL (already wired via `solitaire_server`) and post it elsewhere. The web-viewer already exists.
|
||||||
|
|
||||||
|
## Resume prompt
|
||||||
|
|
||||||
|
```
|
||||||
|
You are a senior Rust + Bevy developer working on Solitaire Quest.
|
||||||
|
Working directory: <Rusty_Solitaire clone path on this machine — local
|
||||||
|
directory may still be named Rusty_Solitare from earlier; that's fine>.
|
||||||
|
Branch: master. Direction is OPEN — v0.16.0 just shipped covering
|
||||||
|
modal scroll fixes, pointer cursor, same-frame focus, and scrim-click
|
||||||
|
dismiss across all six read-only modals.
|
||||||
|
|
||||||
|
State: HEAD at v0.17.0 (solver hints + replay-rate slider on top
|
||||||
|
of v0.16.0). Working tree clean apart from untracked CARD_PLAN.md
|
||||||
|
(intentional).
|
||||||
|
Build: cargo clippy --workspace --all-targets -- -D warnings clean.
|
||||||
|
Tests: 1208 passed / 0 failed.
|
||||||
|
|
||||||
|
READ FIRST (in order, before doing anything):
|
||||||
|
1. SESSION_HANDOFF.md — v0.16.0 changelog + open punch list
|
||||||
|
2. CHANGELOG.md — release-by-release record
|
||||||
|
3. CLAUDE.md — hard rules (UI-first, no panics, etc.)
|
||||||
|
4. ARCHITECTURE.md — crate responsibilities + data flow
|
||||||
|
5. ~/.claude/projects/<this-project>/memory/MEMORY.md
|
||||||
|
— saved feedback / project context (machine-local;
|
||||||
|
may be missing on a fresh machine)
|
||||||
|
|
||||||
|
DECISION TO ASK THE PLAYER FIRST:
|
||||||
|
A. Solver-on-AsyncComputeTaskPool with progress toast + cancel.
|
||||||
|
A previous attempt was rolled back when an agent left 3
|
||||||
|
failing tests; redoing it needs smaller pieces. Eliminates the
|
||||||
|
worst-case 6 s UI stall — highest gameplay impact left.
|
||||||
|
B. Per-deal "won previously" HUD indicator using the rolling
|
||||||
|
replay history's seeds.
|
||||||
|
C. Replay sharing — copyable URL via the existing web viewer.
|
||||||
|
D. Take the deferred desktop-packaging item (needs artwork +
|
||||||
|
signing certs from the user).
|
||||||
|
|
||||||
|
WORKFLOW NOTES:
|
||||||
|
- Commits use:
|
||||||
|
git -c user.name=funman300 -c user.email=root@vscode.infinity \
|
||||||
|
commit -m "..."
|
||||||
|
- When attributing playtester feedback in commits/docs, use "Quat"
|
||||||
|
not "Rhys" (saved feedback memory).
|
||||||
|
- Sub-agents stage + verify only; orchestrator commits.
|
||||||
|
- Every commit must pass build / clippy / test before pushing.
|
||||||
|
- Push to GitHub (origin) — that is the canonical remote.
|
||||||
|
|
||||||
|
OPEN AT THE START: ask which of A–D. Don't pick unilaterally.
|
||||||
|
```
|
||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 283 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 357 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 365 KiB |
|
After Width: | Height: | Size: 179 KiB |