diff --git a/README.md b/README.md index f9485c7..2094edb 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,6 @@ Open a browser window at `http://127.0.0.1:9091`. You can play against a very ba ## Inspirations -The multiplayer game architecture, implemented in packages _clients/backbone-lib_, _clients/web/game_, _server/protocol_, _server/relay-server_ is a Leptos-optimized adaptation of the macroquad-based [Carbonfreezer/multiplayer](https://github.com/Carbonfreezer/multiplayer) project. It is a multiplayer game system in Rust targeting browser-based board games compiled as WASM. The original project used Macroquad with a polling-based transport layer; this version replaces that with an async session API built for [Leptos](https://leptos.dev/). +The multiplayer game architecture, implemented in packages _clients/backbone-lib_, _clients/web/game_, _server/protocol_ and _server/relay-server_ is a [Leptos](https://leptos.dev/)-optimized adaptation of the macroquad-based [Carbonfreezer/multiplayer](https://github.com/Carbonfreezer/multiplayer) project. The web client UX/UI is inspired by https://playtiao.com. diff --git a/doc/client_web_design_proposals.md b/doc/client_web_design_proposals.md deleted file mode 100644 index 598e9c5..0000000 --- a/doc/client_web_design_proposals.md +++ /dev/null @@ -1,307 +0,0 @@ -# client_web — UI/UX Design Proposals - -A structured critique of the current interface compared to the physical game, followed by concrete upgrade proposals. Organised from most impactful to most effort-intensive. - ---- - -## Aesthetic Direction - -**Concept: "18th-century French gaming salon"** - -The physical trictrac board is a piece of furniture — carved mahogany rails, felt or baize surface, ivory and ebony checkers, brass pegs in drilled holes, gilt scoring tokens. The online interface should feel like playing on that table under candlelight in a Parisian salon. - -- **Typography**: Pair a classical serif (e.g. [Cormorant Garamond](https://fonts.google.com/specimen/Cormorant+Garamond)) for headings and score readouts with a clean humanist sans (e.g. [Jost](https://fonts.google.com/specimen/Jost)) for UI controls and status text. -- **Palette**: Forest green felt board (`#1d3d28`), alternating ivory (`#f0e6c8`) and deep burgundy (`#7a1e2a`) triangular fields, dark mahogany rails (`#2a1508`), aged parchment panels (`#f2e8d0`), gilt gold accents (`#c8a448`). -- **Unforgettable detail**: Triangular fields (true *flèches*) rendered in CSS with a wood-grain body surrounding them. - ---- - -## 1. Board Shape: Rectangles → True Triangles - -**Current state**: Fields are 60×180px rectangles with a rounded corner. No backgammon/trictrac board looks like this. - -**Physical game**: Fields are elongated triangles (*flèches*) pointing from the rail toward the center bar, alternating two colors. - -**Proposal**: Replace `.field` `
` elements with SVG triangles, or use CSS `clip-path: polygon(50% 0%, 0% 100%, 100% 100%)` for bottom-row triangles and `clip-path: polygon(0% 0%, 100% 0%, 50% 100%)` for top-row triangles. The field background and checker stack become SVG foreignObject or positioned elements inside. This is a large structural change to `board.rs` but is the single highest-impact visual improvement. - -The board body between triangles becomes visible as the wood/felt surface — this naturally creates the physical board's "relief" without any extra decoration. - ---- - -## 2. Jan Zone Visual Identity - -**Current state**: The four quarters (small jan, big jan, return jan, last jan) are visually identical — same field color scheme, no labels or separation beyond the center bar. - -**Physical game**: Players must constantly know which quarter they are in because the rules differ radically per zone (forbidden jans, filling values, hit scoring values differ between small-jan-table and big-jan-table, corner position, exit zone). - -**Proposals**: - -### 2a. Zone labels -Add thin labels (`"petit jan"`, `"grand jan"`, `"jan de retour"`) beneath the board-row (or as a subtle strip above/below the quarter). These should use the serif font at very small size and low opacity — decorative, not noisy. - -### 2b. Field color shift per zone -The physical game uses alternating colors within each quarter, but different quarters can use slightly different base hues: -- Small jan (fields 1–6): warm ivory / burgundy -- Big jan / corner zone (fields 7–12): same, but field 12 gets a distinct "corner" treatment (see §4) -- Return jan (fields 13–18): very subtly cooler ivory / dark teal instead of burgundy — signals "opponent's territory" -- Last jan / exit (fields 19–24): subtly warmer, indicating checkers are "almost home" - -### 2c. Small-jan-table / big-jan-table highlight during hit scoring -When a hit is being scored, briefly tint the entire table (fields 1–12 or 13–24) to make the point value distinction (4 pts vs 2 pts) spatially obvious. This fires as a 300ms flash synchronized with the scoring notification. - ---- - -## 3. Rest Corner (Field 12 / 13) Special Appearance - -**Current state**: Field 12 looks identical to field 11. Nothing indicates its unique rules (must enter/leave with 2 checkers simultaneously, cannot be landed on by a single checker). - -**Physical game**: The corner is a corner — it is literally in the corner of the table, a distinct physical location. - -**Proposals**: -- Give field 12 (and 13 for Black) a **crown or arch shape** at the tip of the triangle, using a small SVG ornament. -- Apply a **slightly warmer gold** field color to distinguish it. -- When the player has two checkers there, show a subtle **lock icon** or a gilded ring around the checker stack to indicate "corner held." -- When the corner is available to be taken *par puissance*, add a gentle pulsing outline on field 12 to indicate the privilege is available. -- Tooltip or popover: on hover, show a brief note "Coin de repos — must enter and leave with 2 checkers." - ---- - -## 4. Checker Rendering: Static → Animated - -**Current state**: Checkers appear and disappear between `ViewState` snapshots. No movement animation. - -**Physical game**: Checkers slide across the board with a satisfying click sound. - -**Proposals**: - -### 4a. Slide animation -Diff the board array between the previous and current `ViewState`. For each checker that moved from field A to field B, apply a CSS or Web Animation API translation from `field_center(A)` to `field_center(B)` (duration ~250ms, ease-out). This requires keeping the previous `ViewState` as state in `GameScreen` and computing a diff when a new state arrives. - -### 4b. Lift effect during staging -When the player clicks an origin field and a checker becomes "selected," apply a `transform: scale(1.15) translateY(-4px)` with a subtle drop shadow increase. Visually lifts the checker off the board. - -### 4c. Checker appearance -Replace the CSS `radial-gradient` circles with SVG: -- **White**: ivory `#f5edd8` with a pearl sheen gradient, thin gilt ring border, engraved concentric circles -- **Black**: ebony `#1a0f06` with subtle grain texture, same gilt ring - -A stack of 5+ checkers can render a "perspective stack" — each checker at a slight y offset with a shadow, giving depth. - ---- - -## 5. Dice: Static → Rolling Animation - -**Current state**: Dice appear with their final value immediately. No sense of randomness or anticipation. - -**Physical game**: Dice are shaken in a cup (*cornet*) and tumbled out. The roll is a theatrical moment. - -**Proposals**: - -### 5a. Roll animation -When `SerTurnStage` transitions from `RollDice` to `Move`, animate both dice with a fast face-cycling (showing random faces for ~400ms, decelerating to final value). Pure CSS `animation` on the die-face SVG circles, cycling via `keyframes`. - -### 5b. Dice cups -Add two SVG/CSS dice cups above the dice display. During rolling, they visually "tip" (rotate 90° via CSS transform) and the dice "fall out." A subtle translate-y on the dice moves them downward into view. - -### 5c. Double visual -When both dice show the same value, add a subtle golden glow around both — visually communicating that it is a double (which affects scoring: 6 pts instead of 4, etc.). - -### 5d. Used-die visual -When one die has been consumed by a staged move, slide it slightly down and reduce opacity (current: gray-out). Animate the "used" transition with `transition: all 0.15s`. - ---- - -## 6. Scoring Notifications: Side Panel → Layered Toasts - -**Current state**: Scoring events appear as a small cream panel in the side panel column (`scoring-panel`). They are easily missed, especially opponent events. - -**Physical game**: Scoring is the central drama of every turn — points are loudly marked, bredouille doubled, holes recorded with pegs. - -**Proposals**: - -### 6a. Board-overlaid toast for holes -When a hole is won, display a large centered overlay on the board — not a modal, but a translucent toast with gilt border: `"Trou ! ×2 bredouille"`. Auto-dismiss after 1.5s or on click. This is the most important event and deserves the most visual weight. - -### 6b. Scoring event animation -When `my_scored_event` appears, animate the panel sliding in from the right with a 200ms ease-out. Jan rows stagger in (each with `animation-delay: n * 50ms`). - -### 6c. Jan hover → board highlight synchronization -The current arrow-on-hover feature is good. Extend it: when hovering a jan row, also highlight the relevant fields with a faint golden shimmer instead of (or in addition to) the arrows. This ties the abstract jan name to a concrete board location. - -### 6d. Bredouille treatment -Bredouille doubles a hole's value — a massive game event. Currently shown as a small amber badge. Proposals: -- The toast for a bredouille hole should animate in differently: bigger, gold shimmer background -- Show a small animated flag (*pavillon*) icon in the score panel when bredouille is active, matching the physical game's token - -### 6e. Hit scoring visual -When a hit is scored (*battue*), show a brief visual on the opponent's half-field checker — a faint concentric ring expanding outward (CSS `animation: ripple 0.4s ease-out`). This communicates the "fictitious" nature of the hit: something happened at that checker's position, but it didn't move. - ---- - -## 7. Score Panel: Progress Bars → Pegs and Holes - -**Current state**: Points and holes are displayed as progress bars (0–12) and numeric values. Functional but abstract. - -**Physical game**: Points are tracked with physical tokens (*jetons*) placed on the board surface at specific field tips. Holes are tracked with pegs (*fichets*) in holes drilled along the rail at each field base. - -**Proposals**: - -### 7a. Hole tracker: 12 dots/pegs -Replace the `score-bar-holes` progress bar with a row of 12 small circles ("drilled holes") in a horizontal strip. Filled holes are rendered as a gilded peg inserted (solid gold circle). Unfilled holes are empty rings. This is a `` with 12 `` elements. The filled count animates one peg at a time (sequenced `animation-delay`). - -### 7b. Point tracker: token on board -For points (0–11), show a small token image positioned at the corresponding field tip along the near rail — mirroring the physical game exactly. This is ambitious but highly authentic. A simpler approach: replace the thin progress bar with a 12-cell dot track where one glowing token is positioned. - -### 7c. Bredouille indicator -When `can_bredouille` is true for a player, show the token as a double-token (two stacked icons) or add a small flag icon next to the token. - ---- - -## 8. Status Communication: Text → Contextual Guidance - -**Current state**: A single text line (`"Select move 1"`, `"Opponent's turn"`) in the status bar. New players have no idea what to do or why they can't do something. - -**Physical game**: Human players narrate what's happening; experienced players understand the state from context. - -**Proposals**: - -### 8a. Contextual sub-prompt -Below the primary status, show a secondary hint line in smaller text: -- During `Move` stage: `"Click a highlighted field to move a checker"` -- During `HoldOrGoChoice`: `"Hold to keep points and keep playing — Go to reset and start a new setting"` -- When waiting for confirm: `"↑ Opponent scored points — click Continue when ready"` - -### 8b. Forbidden-jan visual cue -When a field is in the opponent's jan and the player cannot land there (forbidden jan rule), show those fields with a subtle `✕` pattern or darker tint rather than just being unclickable. This communicates *why* the fields aren't selectable. - -### 8c. Exit-eligible highlight -When all player checkers are in the last jan (fields 19–24), add a subtle directional glow to the exit rail (the right/left edge of the board depending on player). A small "EXIT →" arrow indicator could appear. - -### 8d. Can-take-corner indicator -When the player can take their corner (field 12 or 13 is the valid destination), add a brief pulse to that field beyond the standard `.dest` highlight — the corner rules are special enough to warrant extra visual salience. - ---- - -## 9. Bug Fix: Hold Button Is Non-Functional - -**File**: `src/components/scoring.rs` line 91 - -The "Hold" button in the `ScoringPanel` has no `on:click` handler. In the physical game, "Hold" (*tenir*) means: stay in the current setting, mark remainder points, and continue playing normally. - -`PlayerAction` does not currently include a `Hold` variant. In the current implementation, if the player simply does nothing (doesn't click Go), the game waits — but there is no message sent to the backend to confirm "staying." - -**Fix required**: Add `PlayerAction::Hold` (or reuse `Mark`) and connect the Hold button's `on:click` to send it. The backend needs to handle it by advancing past `HoldOrGoChoice` without triggering `GameEvent::Go`. - ---- - -## 10. Layout: Side Panel → Integrated Design - -**Current state**: The board and side panel sit side-by-side (`board-and-panel: flex-direction row`). The side panel (min-width 160px) contains status, dice, scoring, and buttons stacked vertically. - -**Proposals**: - -### 10a. Move dice inside the board -Place the dice display centered in the **board-bar** (the vertical divider between quarters). Currently the bar is 20px wide — widen it to ~80px and center two dice there. This puts dice physically near the board action, matching the physical game where dice land on the board surface. The bar color becomes a darker felt strip. - -### 10b. Status bar above the board -Move the primary status message to a full-width strip directly above the board, styled with the serif font at larger size. This gives it appropriate visual weight and removes it from the cramped side panel. - -### 10c. Action buttons below the board (or in score panels) -"Continue," "Go," and "Hold" buttons can live below the board in a centered button row. The side panel then becomes purely informational (scoring panels), which can slide in from the right. - -### 10d. Mobile: rotate board 90° option -The board is ~776px wide. On narrow screens, offer a portrait mode where the board is rendered rotated 90° (each player's quarters stacked vertically), with a scroll-independent panel above/below for controls. - ---- - -## 11. Login Screen: Form → Atmosphere - -**Current state**: A plain 320px-wide column with a `

Trictrac

`, a text input, and three buttons. Functional but gives no sense of what the game is. - -**Physical game**: A trictrac board is an object of beauty — players set it out, prepare the checkers, and roll for first-move privilege. - -**Proposals**: - -### 11a. Illustrated header -A high-quality SVG illustration of the board (simplified top-down view, showing the triangular fields, checker stacks at starting positions, dice) as the page hero. Possibly animated: the two stacks slowly deploying two checkers as the page loads. - -### 11b. Typography treatment -"TRICTRAC" as a large display heading in a classical-weight serif, possibly with subtle tracking and a gilt color. Below it, the French subtitle: *"Jeu de trictrac — XVIIIe siècle"* in small-caps at reduced opacity. - -### 11c. Mode selection -The three buttons (Create / Join / vs Bot) styled as wooden tiles or embossed cards rather than plain buttons. - ---- - -## 12. Game-Over Modal: Generic → Ceremonial - -**Current state**: A centered modal with "Game Over," the winner's name, and Quit/Play Again buttons. - -**Physical game**: The end of a game involves settling accounts, noting the final hole count, and potentially recording results. - -**Proposals**: -- Show a **final score parchment** — both players' hole counts displayed like a ledger entry, with the winner's name engraved in gilt text -- Animate the modal entrance with a slight downward reveal (the parchment "unrolling") -- Show the hole difference: `"8 — 3"` in large numerals with a small flourish between them -- If bredouille applied to the winning holes: `"✕ 2 bredouille"` annotation -- "Play again" styled as "Rejouer" / "Play again" with a dice icon - ---- - -## Implementation Priority - -| Priority | Proposal | Effort | Impact | -|----------|-----------|--------|--------| -| 1 | §9 Fix Hold button (bug) | Low | Correctness | -| 2 | §3 Rest corner special appearance | Low | Clarity | -| 3 | §8b–d Forbidden jan + exit + corner cues | Medium | Clarity | -| 4 | §5a–d Dice roll animation | Medium | Delight | -| 5 | §6a–b Scoring toasts + animation | Medium | Drama | -| 6 | §7a Hole tracker (12 peg dots) | Low | Authenticity | -| 7 | §2a–b Jan zone labels + color shift | Low | Orientation | -| 8 | §4a Checker slide animation | High | Polish | -| 9 | §1 Triangular fields | High | Authenticity | -| 10 | §10a–b Dice in bar + status above board | Medium | Layout | -| 11 | §6e Hit ripple animation | Medium | Comprehension | -| 12 | §11 Login redesign | Medium | First impression | -| 13 | §12 Game-over modal | Low | Finish | -| 14 | §4c SVG checkers | Medium | Aesthetics | -| 15 | §7b–c Token tracker on rail | High | Authenticity | - ---- - -## Typography and CSS Variables Proposal - -Replace the anonymous `sans-serif` body font and introduce a CSS variable system: - -```css -@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;600&family=Jost:wght@300;400;500&display=swap'); - -:root { - /* Board */ - --board-felt: #1d3d28; - --board-rail: #2a1508; - --field-ivory: #f0e6c8; - --field-burgundy: #7a1e2a; - --field-corner: #c8a030; /* rest corner accent */ - --field-exit-glow: #e8c060; - - /* Checkers */ - --checker-white: #f5edd8; - --checker-black: #1a0f06; - --checker-ring: #c8a448; /* gilt border */ - - /* UI */ - --ui-parchment: #f2e8d0; - --ui-parchment-dark: #e4d8b8; - --ui-ink: #2a1a08; - --ui-gold: #c8a448; - --ui-gold-dark: #8a6a28; - --ui-green-accent: #3a6b2a; - --ui-red-accent: #7a1e2a; - - /* Typography */ - --font-display: 'Cormorant Garamond', Georgia, serif; - --font-ui: 'Jost', system-ui, sans-serif; -} -``` diff --git a/doc/client_web_design_proposals_alternative.md b/doc/client_web_design_proposals_alternative.md deleted file mode 100644 index 5e2e361..0000000 --- a/doc/client_web_design_proposals_alternative.md +++ /dev/null @@ -1,381 +0,0 @@ -# client_web — Alternative Design Proposals: Neon Arcade Future - -A second aesthetic direction: bold, playful, unapologetically modern. Where the first proposal channels an 18th-century gaming salon, this one asks: *what if trictrac ran on a holographic table in a Tokyo arcade, 2089?* - -This document proposes a complete visual redesign with no obligation to mirror physical game objects. The priority is delight, readability, and memorability. - ---- - -## Aesthetic Direction: "Holographic Arcade" - -**Core concept**: The board floats in dark space as a self-illuminated slab. Fields pulse with neon light. Checkers are luminous marbles that leave light trails as they move. Scoring events trigger particle explosions. Every interaction has a micro-animation. - -**The one unforgettable thing**: When a hole is won, the entire board floods with a colour wave — a full-screen shimmer that fades in 800ms — like a pinball machine tilting into multiball. - -**Color palette**: Built on darkness, with high-saturation accents — cyan, magenta, gold. Not gradients on white (the generic AI aesthetic); instead, near-black backgrounds with glowing, luminous elements. - -**Typography**: [Space Grotesk](https://fonts.google.com/specimen/Space+Grotesk) is overused. Instead: -- Display: [Syne](https://fonts.google.com/specimen/Syne) — geometric, confident, slightly alien -- Numerics: [DM Mono](https://fonts.google.com/specimen/DM+Mono) — for scores, dice values, field numbers — crisp monospace with personality -- UI labels: [Outfit](https://fonts.google.com/specimen/Outfit) — friendly, modern, clear at small sizes - -```css -:root { - /* Base */ - --void: #09090f; /* near-black with blue tint */ - --surface: #12121f; /* board slab */ - --surface-raised: #1a1a2e; /* panels, cards */ - --surface-glass: rgba(255,255,255,0.05); /* glassmorphism */ - - /* Neon accents */ - --cyan: #00e5ff; - --cyan-dim: #0099bb; - --magenta: #e040fb; - --gold: #ffd740; - --gold-dim: #c8a820; - --green-neon: #69ff47; - --orange-neon: #ff6d3a; - - /* Player colors */ - --player-white: #e8e0ff; /* soft violet-white */ - --player-black: #1a0040; /* deep indigo-black */ - --player-white-glow: #b39ddb; - --player-black-glow: #7c4dff; - - /* Typography */ - --font-display: 'Syne', sans-serif; - --font-mono: 'DM Mono', monospace; - --font-ui: 'Outfit', sans-serif; - - /* Glow radii */ - --glow-sm: 0 0 8px; - --glow-md: 0 0 16px; - --glow-lg: 0 0 32px; -} -``` - ---- - -## 1. Board: A Floating Holographic Slab - -**Concept**: The board is a dark rectangular surface that appears to float — slight perspective tilt (CSS `perspective` + `rotateX(3deg)`), a thin neon border (1px cyan on top, 1px dimmer on bottom for depth), and a subtle inner glow that makes the board feel luminous from within. - -```css -.board { - background: var(--surface); - border: 1px solid var(--cyan-dim); - box-shadow: - 0 0 0 1px rgba(0,229,255,0.1), - 0 0 40px rgba(0,229,255,0.08), - 0 24px 60px rgba(0,0,0,0.8); - transform: perspective(1200px) rotateX(2deg); - transform-origin: center bottom; - border-radius: 4px; -} -``` - -The board background gets a very subtle **noise texture overlay** (SVG `` or a PNG grain layer at 3% opacity) — just enough to prevent it from looking like a flat rectangle, giving it material presence. - -The center bar and side bars become **glowing dividers**: 4px wide, gradient from `var(--cyan)` at top to `var(--magenta)` at bottom, with a matching glow. - ---- - -## 2. Fields: Neon Triangles with Zone Color Identity - -Triangular fields (CSS `clip-path: polygon`) are essential here — they're geometric and modern, not just historically authentic. - -Each quarter gets its **own neon color identity**, using a very dark base with a glowing triangle border: - -| Quarter | Fields | Primary accent | Secondary (alternating) | -|---------|--------|---------------|------------------------| -| Small jan | 1–6 | `#00e5ff` (cyan) | `#0077aa` (dim cyan) | -| Big jan | 7–12 | `#7c4dff` (violet) | `#4a2a99` (dim violet) | -| Return jan | 13–18 | `#e040fb` (magenta) | `#991a99` (dim magenta) | -| Last jan | 19–24 | `#ffd740` (gold) | `#aa8800` (dim gold) | - -The field itself is dark (`#14141f`). The color lives in a **glowing triangle border** — achieved with a layered `clip-path` + `::before` pseudo-element 2px larger that shows through as the border, with a CSS `filter: blur(3px)` outer glow: - -```css -.field { - background: #14141f; - clip-path: polygon(50% 0%, 0% 100%, 100% 100%); - position: relative; -} -.field::before { - content: ''; - position: absolute; - inset: -2px; - background: var(--field-accent-color); - clip-path: polygon(50% 0%, 0% 100%, 100% 100%); - filter: blur(4px); - opacity: 0.4; - z-index: -1; -} -``` - -**On hover (clickable fields)**: the glow intensifies (`opacity: 0.9`, `filter: blur(6px)`) and the field interior lightens slightly. A ripple animation radiates outward from the click point. - -**Selected field**: the entire field interior fills with a semi-transparent neon color — not just the border — and a 2px dashed animated border spins around it (`animation: spin-border 1s linear infinite`). - ---- - -## 3. Checkers: Luminous Marbles - -Forget CSS circles with radial gradients. Each checker is a **glowing orb** with: - -- A dark, slightly translucent core -- A radial highlight in the upper-left (simulating a point light source) -- A colored halo that radiates outward onto the field triangle -- A subtle inner reflection ring - -```css -.checker.white { - background: radial-gradient(circle at 35% 30%, - #ffffff, - #c8c0e0 40%, - #8878c0 70%, - #3a2a60 - ); - box-shadow: - inset 0 2px 6px rgba(255,255,255,0.8), - inset 0 -2px 4px rgba(0,0,0,0.4), - 0 0 12px rgba(179,157,219,0.6), /* violet-white glow */ - 0 0 24px rgba(124,77,255,0.3); /* outer violet halo */ -} - -.checker.black { - background: radial-gradient(circle at 35% 30%, - #7c4dff, - #4a2d99 40%, - #1a0a40 70%, - #09040f - ); - box-shadow: - inset 0 2px 6px rgba(124,77,255,0.5), - inset 0 -2px 4px rgba(0,0,0,0.8), - 0 0 12px rgba(124,77,255,0.7), - 0 0 24px rgba(124,77,255,0.3); -} -``` - -**Stack depth**: A stack of N checkers renders with each checker offset by 6px vertically and slightly scaled (0.97× per level deeper), creating genuine 3D stack depth without any 3D CSS transform. The count label floats above as a monospace number in `var(--gold)`. - -**Selection animation**: On click to select, the top checker of the stack does a quick `scale(1.2) translateY(-8px)` bounce (150ms spring easing), then settles at `scale(1.1) translateY(-4px)` while selected. - -**Movement animation**: When a move is confirmed (board state diff), selected checkers do a **light-trail arc** — a bezier path from origin field center to destination, with a fading cyan streak left behind (`box-shadow` animated along the path via `@property` interpolation or JS Web Animation API). Duration: 300ms. - ---- - -## 4. Dice: Holographic Crystals - -Replace the SVG ivory dice with **translucent crystal cubes**: - -- Each die face is a dark glass square with a thin neon border -- Pips are glowing dots — cyan for normal, gold for doubles -- The die face has a subtle `backdrop-filter: blur(4px)` on a glass background - -```css -.die-face rect { - fill: rgba(255, 255, 255, 0.04); - stroke: var(--cyan); - stroke-width: 1.5; - rx: 6; - filter: drop-shadow(0 0 6px var(--cyan)); -} -.die-face circle { - fill: var(--cyan); - filter: drop-shadow(0 0 4px var(--cyan)); -} -``` - -**Double dice**: Both pips and borders switch to `var(--gold)`, with a stronger glow (`drop-shadow(0 0 8px var(--gold))`). - -**Roll animation**: 600ms sequence — -1. Both dice **shatter outward** (`scale(0) rotate(720deg)`, opacity 0 → 1) appearing from nothing -2. During 400ms they rapidly cycle through face values (random pips swap every 60ms via CSS `animation`) -3. Final 200ms they decelerate and **snap** to the rolled values with a brief flash pulse - -**Used die**: Fades the border to `rgba(255,255,255,0.1)` and dims pips to `rgba(255,255,255,0.2)` — the die goes "offline." A thin strikethrough line appears diagonally. - ---- - -## 5. The Hole Tracker: Orbital Rings - -Instead of progress bars, score and hole progress are visualised as **concentric orbital rings** beside each player's name panel — inspired by loading spinners, but static and data-driven. - -- **Outer ring** (thick, 6px): hole progress. 12 segments, each one lights up as a hole is won. Segments are `var(--gold)` when won, near-invisible dark when empty. -- **Inner ring** (thin, 3px): point progress within the current hole. Continuously filled arc from 0° to (points/12 × 360°). Color: `var(--cyan)` for the active player, `var(--magenta)` for the opponent. - -The arc fills animate with `stroke-dashoffset` transition (0.4s ease-out) on every point gain. - -**Bredouille state**: The outer ring segments pulse — a slow `opacity: 0.6 → 1 → 0.6` sinusoidal glow — as long as bredouille is active. A small flag icon (⚑) in `var(--gold)` appears beside the ring. - ---- - -## 6. Scoring Events: Light Shows - -### Hole won — Full-board colour wave -A `position:fixed` `::after` overlay expands from the scoring player's side of the board: -- Radial gradient expanding from one edge: `rgba(255,215,64,0)` → `rgba(255,215,64,0.15)` → `rgba(255,215,64,0)` -- Duration: 800ms, ease-in-out -- Simultaneously: the scoring player's orbital rings segments animate sequentially (each segment snaps on with a 50ms delay) -- A large centered text `"+1 TROU"` in `var(--font-display)` at 3rem scales from 60% to 110% with `opacity: 0 → 1 → 0`, duration 1.2s - -### Bredouille — The cascade -On top of the hole wave, add: -- A **confetti burst** of small colored squares (pure CSS: 20 `` elements with randomised `animation-delay` and `translate`/`rotate` keyframes) in cyan, magenta, gold -- The `"+1 TROU"` text instead reads `"BREDOUILLE ×2"` in `var(--magenta)` -- The board border flashes: `border-color` cycles cyan → magenta → gold → cyan over 0.6s - -### Jan scored — Notification card -Each jan scored gets a **toast card** that slides in from the right edge: -- Dark glass background (`rgba(26,26,46,0.95)`) with a left border in the jan's quarter color -- Jan name in `var(--font-ui)` bold, points in `var(--font-mono)` large -- Progress: `"+4 pts"` in cyan, `"+6 pts (double)"` in gold -- Cards stack vertically if multiple jans fire; each staggered by 80ms -- Auto-dismiss with a rightward slide-out after 3s - -### Hit scored — Ripple on the target checker -When a hit is scored on a specific field, that field's checker emits a **sonar ripple**: -- 3 concentric rings expand from the checker's center, each `opacity: 1 → 0, scale: 1 → 2.5` -- Color: cyan for true hits, magenta for false hits (giving to opponent) -- Duration: 600ms per ring, staggered by 200ms - ---- - -## 7. Player Panels: Glassmorphism Cards - -Replace the cream `background: #f5edd8` panels with **glass cards** floating above the void: - -```css -.player-score-panel { - background: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.1); - border-top: 1px solid rgba(255, 255, 255, 0.2); /* top catches light */ - backdrop-filter: blur(12px) saturate(1.5); - border-radius: 12px; - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.4), - inset 0 1px 0 rgba(255, 255, 255, 0.08); -} -``` - -**Active player panel**: the border glow of the active player's card brightens: `border-color: var(--cyan)` with `box-shadow: 0 0 16px rgba(0,229,255,0.2)`. A tiny animated pulse on the left edge (`width: 3px, animation: pulse 1.5s ease-in-out infinite`) indicates it is their turn. - -**Player name**: Displayed in `var(--font-display)` at 1.1rem. A small colored dot (cyan for player 1, magenta for player 2) precedes the name — acts as the "you" indicator without needing a text suffix. - ---- - -## 8. Status Bar: Dynamic Ambient Messaging - -Replace the single plain-text status line with a **contextual bar** that changes character per game stage: - -| Stage | Style | Color | -|-------|-------|-------| -| Waiting for opponent | Slow pulsing dots animation `... ` | Dim white | -| Your turn (roll) | "YOUR MOVE" in `var(--font-display)` with a blinking cursor | Cyan | -| Opponent's turn | Subtle shimmer on text | Dim magenta | -| Move selection | "SELECT CHECKER ①" with animated underline on "SELECT" | Cyan | -| Hold or Go | "SCORE!" with a spinning star ✦ | Gold | -| Paused (continue) | The bar has a pulsing amber background strip | Amber | -| Game over | Text cycles through all player colors | Full rainbow | - -The bar itself is 3px tall and spans the full board width, showing a **neon progress shimmer** during the opponent's turn (a traveling gleam, like CSS `animation: shimmer` on a gradient). - ---- - -## 9. Jan Zone Awareness: Neon Underlay - -Rather than labels, the four quarters glow with their zone color in the background of the board — very subtle, just 4% opacity fills under the triangles: - -```css -.board-quarter-small-jan { background: rgba(0, 229, 255, 0.04); } -.board-quarter-big-jan { background: rgba(124, 77, 255, 0.04); } -.board-quarter-return-jan { background: rgba(224, 64, 251, 0.04); } -.board-quarter-last-jan { background: rgba(255, 215, 64, 0.04); } -``` - -When hovering a scoring-notification row that references a specific jan (e.g. "Big jan conserved"), the corresponding quarter's background pulses from 4% → 15% opacity for 600ms. This replaces the arrow overlay with a spatial, zone-level highlight — more legible and visually coherent. - ---- - -## 10. Rest Corner: The Crown Field - -Field 12 (White) and 13 (Black) get a distinct appearance: - -- The triangle is outlined in `var(--gold)` instead of its quarter's color -- A small **crown SVG** (⚜ or ♛) floats centered in the triangle at 30% opacity when empty, brighter when held -- When the player holds the corner (2 checkers there), the triangle interior fills with a very subtle gold shimmer animation (`background-position: 0% → 100%` on a diagonal gradient, 2s loop) -- When the corner is available to be taken *par puissance*, the crown pulses at 1Hz - ---- - -## 11. Login Screen: Warp Speed Entrance - -**Hero**: A dark void with an animated **particle field** — small white/cyan dots drifting slowly, like stars. Pure CSS with 50 `` elements (or a single `` for performance), each with randomised `animation-delay` and drift keyframe. - -**Title**: "TRICTRAC" in `var(--font-display)` at 5rem, with a **chromatic aberration effect** — three slightly offset copies in cyan, magenta, and white, blended with `mix-blend-mode: screen`. The word appears with a `clip-path: inset(100% 0 0 0) → inset(0% 0 0 0)` reveal animation (the text "rises" into view). - -**Tagline**: `"XVIIIe siècle · En ligne · ∞"` in `var(--font-mono)` at 0.85rem, appearing letter-by-letter with a 20ms interval. - -**Mode cards**: Instead of three buttons, three **holographic tiles** in a row: -- Each is a glass card with an icon, label, and a colored accent strip on the bottom -- On hover: the card lifts (`translateY(-4px)`) and the bottom strip color floods the card (low opacity fill) -- CREATE: cyan accent; JOIN: violet accent; vs BOT: orange accent - -**Room code input**: Dark glass input with a cyan border glow on focus, monospace font for the code, no placeholder text (just a blinking cursor showing it's ready). The input border animates a traveling gleam on focus. - ---- - -## 12. Game-Over Screen: Score Reveal Ceremony - -Instead of a modal over a frozen game, the game-over sequence is a **full-page takeover**: - -1. **Board fades out** (800ms fade): the board dims to 20% opacity -2. **Score card rises** from the bottom: a tall glass card with both players' hole counts displayed large in `var(--font-display)` — `"8"` vs `"3"` — in their respective colors -3. **Winner highlight**: the winning number scales up to 200% with a gold burst radiation behind it -4. **Bredouille annotation**: if applicable, `"× 2"` appears beside the number with a magenta glow, then the number updates to the effective doubled count -5. **Continue options**: two buttons slide up last — "QUIT" and "REJOUER" — with the rejouer button pulsing in cyan - ---- - -## 13. Global Micro-Interactions - -These apply throughout and give the interface a consistently tactile feel: - -- **Button press**: `scale(0.96)` on `:active`, 80ms, then spring back. No `opacity` change — scale is more physical. -- **Button focus**: neon outline ring animated in from 0 to full radius (not the browser default outline). -- **Panel hover**: glass cards shift `box-shadow` slightly for a lifted feel. -- **Page load**: all elements stagger in with a `translateY(10px) → 0 + opacity 0 → 1`, each component with a `animation-delay` offset (board: 0ms, panels: 100ms, side panel: 200ms). -- **Custom cursor** (optional): replace the default cursor with a small circle that trails slightly behind the real cursor position — creates a luxurious "lag" feeling. Pure JS: interpolate cursor position toward mouse position at 80% each frame. - ---- - -## Implementation Notes for Leptos/WASM - -### What's straightforward in pure CSS -- All color variables, glass panels, glow effects, orbital rings (SVG `stroke-dashoffset`) -- Dice roll animation (CSS keyframes) -- Toast slide-ins (CSS `@keyframes` + `animation`) -- Confetti (CSS `@keyframes` on positioned `
` elements) -- Particle field on login (CSS-only with many `` elements) - -### What needs a small JS/WASM component -- **Board perspective tilt** with mouse-tracking (subtle parallax) — `mouse_position` signal driving CSS custom property -- **Checker light-trail movement** — needs previous/next board diff, then Web Animation API or `requestAnimationFrame` -- **Chromatic aberration on title** — CSS filter or SVG filter, but the animation needs JS timing - -### What needs Rust/Leptos state -- **Board diff for animation**: store previous `[i8; 24]` alongside current in `GameScreen` as a `Memo`, compute moved checkers -- **Event timing for sequences**: hole-won wave → score reveal → dismiss must be orchestrated; a `RwSignal>` in `GameScreen` drives each phase - -### Progressive approach -The proposals above can be adopted incrementally. Suggested order: -1. CSS variables + dark theme + Syne/DM Mono fonts → immediate impact, zero logic change -2. Glass panels, neon borders, glow effects → pure CSS -3. Orbital ring score tracker → SVG component -4. Triangular fields + zone colors → `board.rs` structural change -5. Dice animation → CSS keyframes in `die.rs` -6. Toast notifications → new `toast.rs` component -7. Hole-won wave → CSS overlay + Leptos signal -8. Checker animation → board diff + Web Animation API diff --git a/doc/client_web_overview.md b/doc/client_web_overview.md index 980ea2f..4cdabcf 100644 --- a/doc/client_web_overview.md +++ b/doc/client_web_overview.md @@ -7,21 +7,21 @@ A Leptos-based WASM frontend for trictrac. Builds to a single-page app served by ## File Structure ``` -client_web/ +clients/web/ ├── Cargo.toml # Dependencies and i18n locale config ├── Trunk.toml # Serve port 9092 ├── index.html # Shell: mounts WASM + links CSS ├── assets/style.css # All styles (~472 lines, no framework) ├── locales/ -│ ├── en.json # 52 English keys -│ └── fr.json # 52 French keys +│ ├── en.json # English keys +│ └── fr.json # French keys └── src/ ├── main.rs # load_locales!() macro + mount_to_body - ├── app.rs # Root App component, state, network loop (571 lines) + ├── app.rs # Root App component, state, network loop ├── components/ │ ├── mod.rs - │ ├── game_screen.rs # Main in-game UI, move staging (324 lines) - │ ├── board.rs # Board rendering and click handling (372 lines) + │ ├── game_screen.rs # Main in-game UI, move staging + │ ├── board.rs # Board rendering and click handling │ ├── die.rs # SVG die face │ ├── score_panel.rs # Points/holes bar for one player │ ├── scoring.rs # Jan-by-jan scoring notification panel @@ -29,9 +29,9 @@ client_web/ │ └── connecting_screen.rs └── trictrac/ ├── mod.rs - ├── types.rs # Protocol types: ViewState, JanEntry, PlayerAction, … (217 lines) - ├── backend.rs # BackEndArchitecture impl, engine bridge (332 lines) - └── bot_local.rs # Local bot: random moves, always Go (34 lines) + ├── types.rs # Protocol types: ViewState, JanEntry, PlayerAction, … + ├── backend.rs # BackEndArchitecture impl, engine bridge + └── bot_local.rs # Local bot: random moves, always Go ``` --- @@ -75,21 +75,21 @@ Playing ──(disconnect / game over)──→ Login ### Root signals (live in `App`, provided via Leptos context) -| Signal | Type | Purpose | -|--------|------|---------| -| `screen` | `RwSignal` | Which screen is shown | +| Signal | Type | Purpose | +| --------- | --------------------------------- | ----------------------------------- | +| `screen` | `RwSignal` | Which screen is shown | | `pending` | `RwSignal>` | Buffered states awaiting "Continue" | -| `cmd_tx` | `UnboundedSender` | UI → network command channel | +| `cmd_tx` | `UnboundedSender` | UI → network command channel | Both `pending` and `cmd_tx` are provided as context so any descendant can read/write them without prop-drilling. ### GameScreen-local signals -| Signal | Type | Purpose | -|--------|------|---------| -| `selected_origin` | `RwSignal>` | First clicked field during move staging | -| `staged_moves` | `RwSignal>` | Accumulated (origin, dest) pairs for this turn | -| `hovered_jan_moves` | `RwSignal>` | Moves to draw arrows for on hover | +| Signal | Type | Purpose | +| ------------------- | ------------------------------------------- | ---------------------------------------------- | +| `selected_origin` | `RwSignal>` | First clicked field during move staging | +| `staged_moves` | `RwSignal>` | Accumulated (origin, dest) pairs for this turn | +| `hovered_jan_moves` | `RwSignal>` | Moves to draw arrows for on hover | ### Data flow @@ -108,7 +108,8 @@ User clicks Go/Continue → cmd_tx.send or pending.pop_front() ## Network and Session -The multiplayer layer is provided by `backbone-lib` (local fork at `../../forks/multiplayer/`). `App` spawns an async task (via `spawn_local`) that multiplexes: +The multiplayer layer is provided by `backbone-lib`. `App` spawns an async task (via `spawn_local`) that multiplexes: + - `cmd_rx`: commands from UI components - `session.next_event()`: updates from the server @@ -135,11 +136,11 @@ Certain opponent events are paused so the local player can see what happened bef Pause triggers (`infer_pause_reason()` in `app.rs`): -| Reason | Condition | -|--------|-----------| -| `AfterOpponentRoll` | Opponent is active; dice values changed | -| `AfterOpponentGo` | Opponent chose Go (HoldOrGoChoice→Move transition) | -| `AfterOpponentMove` | Turn switched to us | +| Reason | Condition | +| ------------------- | -------------------------------------------------- | +| `AfterOpponentRoll` | Opponent is active; dice values changed | +| `AfterOpponentGo` | Opponent chose Go (HoldOrGoChoice→Move transition) | +| `AfterOpponentMove` | Turn switched to us | While a state is in the pending queue, `GameScreen` shows a "Continue" button. Clicking it calls `pending.pop_front()`; if the queue empties, the live state is displayed. @@ -153,18 +154,19 @@ While a state is in the pending queue, `GameScreen` shows a "Continue" button. C ### PlayerAction → GameEvent mapping -| PlayerAction | GameEvents emitted | -|---|---| -| `Roll` | `GameEvent::Roll`, `GameEvent::RollResult(d1, d2)` | -| `Move(m1, m2)` | `GameEvent::Move` (after validation) | -| `Go` | `GameEvent::Go` | -| `Mark` | internal; drives `MarkPoints`/`MarkAdvPoints` loop automatically | +| PlayerAction | GameEvents emitted | +| -------------- | ---------------------------------------------------------------- | +| `Roll` | `GameEvent::Roll`, `GameEvent::RollResult(d1, d2)` | +| `Move(m1, m2)` | `GameEvent::Move` (after validation) | +| `Go` | `GameEvent::Go` | +| `Mark` | internal; drives `MarkPoints`/`MarkAdvPoints` loop automatically | `drive_automatic_stages()` loops through scoring stages without waiting for player input — these are not interactive in the current implementation (schools are not implemented). ### ViewState construction `ViewState::from_game_state()` in `types.rs` converts the engine state to the serialisable snapshot sent to clients: + - `board: [i8; 24]` — direct copy of `Board::positions` - `dice: [u8; 2]` — current dice values - `stage / turn_stage` — serialisable enums (`SerStage`, `SerTurnStage`) @@ -175,6 +177,7 @@ While a state is in the pending queue, `GameScreen` shows a "Continue" button. C ### Bot `bot_local.rs` runs in the browser (no server call). It inspects `GameState` directly and returns a `PlayerAction`: + - **RollDice**: always Roll - **HoldOrGoChoice**: always Go - **Move**: picks a random legal sequence from `MoveRules::get_possible_moves_sequences()`; mirrors moves because Black's board is mirrored @@ -205,11 +208,11 @@ Fields are 60 × 180 px, alternating gold (`#d4a843` / `#c49030`). Checkers are Field CSS classes are computed reactively inside the `view!` macro closure: -| Class | Meaning | -|-------|---------| -| `.clickable` | Valid origin during Move stage (lime green) | -| `.selected` | Currently selected origin (darker green + outline) | -| `.dest` | Valid destination for the selected origin | +| Class | Meaning | +| ------------ | -------------------------------------------------- | +| `.clickable` | Valid origin during Move stage (lime green) | +| `.selected` | Currently selected origin (darker green + outline) | +| `.dest` | Valid destination for the selected origin | `valid_sequences` (from `MoveRules`) is computed once per render and used to derive `valid_origins_for()` and `valid_dests_for()`. The displayed checker count (`displayed_value()`) accounts for staged-but-not-yet-sent moves so the board previews the move visually. @@ -222,6 +225,7 @@ When the player hovers a row in the ScoringPanel, the corresponding checker move ## Scoring Display (`scoring.rs`, `score_panel.rs`) `compute_scored_event()` in `app.rs` diffs consecutive `ViewState` snapshots to produce a `ScoredEvent`: + - `points_earned: i32` - `holes_gained: u8` - `jans: Vec` — only events relevant to the beneficiary @@ -237,6 +241,7 @@ When the player hovers a row in the ScoringPanel, the corresponding checker move `leptos_i18n::load_locales!()` is a compile-time macro that reads `locales/en.json` and `locales/fr.json` and generates a typed `i18n` module. There are 52 keys covering UI labels, game-state prompts, jan names, and status messages. Usage in components: + ```rust let i18n = use_i18n(); t!(i18n, your_turn_roll) // → reactive View @@ -249,9 +254,10 @@ The language switcher (top bar and login screen) calls `i18n.set_locale(Locale:: ## Styling -`assets/style.css` is a single hand-written stylesheet (~472 lines). No CSS framework. +`assets/style.css` is a single hand-written stylesheet. No CSS framework. Key design tokens: + - Body background: `#c8b084` (tan) - Board background: `#2e6b2e` (dark green) - Fields: `#d4a843` / `#c49030` (gold alternating) @@ -264,15 +270,15 @@ Layout uses Flexbox and CSS Grid throughout. Score bars animate with `transition ## Protocol Types (`types.rs`) -| Type | Role | -|------|------| -| `PlayerAction` | `Roll \| Move(CheckerMove, CheckerMove) \| Go \| Mark` — UI → backend | -| `GameDelta` | `{ state: ViewState }` — broadcast to all clients on every change | -| `ViewState` | Full serialisable snapshot of engine state | -| `JanEntry` | One scoring event: jan type, points, ways, moves, is_double | -| `ScoredEvent` | Points/holes delta + jan list for one player in one turn | -| `PlayerScore` | name, points (0–11), holes (0–12), can_bredouille | -| `SerStage` | `PreGame \| InGame \| Ended` | +| Type | Role | +| -------------- | ---------------------------------------------------------------------------------- | +| `PlayerAction` | `Roll \| Move(CheckerMove, CheckerMove) \| Go \| Mark` — UI → backend | +| `GameDelta` | `{ state: ViewState }` — broadcast to all clients on every change | +| `ViewState` | Full serialisable snapshot of engine state | +| `JanEntry` | One scoring event: jan type, points, ways, moves, is_double | +| `ScoredEvent` | Points/holes delta + jan list for one player in one turn | +| `PlayerScore` | name, points (0–11), holes (0–12), can_bredouille | +| `SerStage` | `PreGame \| InGame \| Ended` | | `SerTurnStage` | `RollDice \| RollWaiting \| MarkPoints \| HoldOrGoChoice \| Move \| MarkAdvPoints` | `CheckerMove` comes directly from `trictrac-store`; fields are 1-indexed (0 = stack/exit).