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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions client/engine/ambiance.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,12 @@

function init() {
if (state.inited) return;
// Background ambiance DISABLED: no animated backdrop (nebulas/stars/floaters),
// no floating deco gems, no CSS mascot, no pointer parallax — the game uses a
// clean, static temple background with the real character image on top. Leaving
// early keeps `state.inited` false so every react() call is a safe no-op.
return;
// eslint-disable-next-line no-unreachable
buildBackdrop();
buildStage();
installPointer();
Expand Down
96 changes: 77 additions & 19 deletions client/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,10 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// renderer animation methods + animationSleep. Fast-stop still wins and
// collapses to near-instant.
const turboScale = () => (state.turbo ? 0.4 : 1);
// On fast-stop, the in-flight drop is re-timed to land smoothly in ~this window
// (a quick, visible settle — NOT an instant teleport), and the per-step drop
// awaits resolve this long after the tap so the board never commits mid-fall.
const FAST_STOP_SETTLE_MS = 150;
const animationSleep = (ms) =>
sleep(state.fastStopRequested ? Math.min(50, ms) : Math.round(ms * turboScale()));
const lerp = (a, b, t) => a + (b - a) * t;
Expand Down Expand Up @@ -650,23 +654,56 @@ class ReelCanvasRenderer {
try { this.draw(); } catch (err) { console.error("forceDrawNow draw failed:", err); }
}

requestFastStop() {
/** Re-time an in-flight drop so it CONTINUES from its current visual position
* and finishes smoothly in ~`remainingMs`, i.e. it appears to speed up rather
* than snapping. (The old code jumped progress to 0.75 instantly, which read
* as a glitchy teleport.) Preserves the current progress `p` and compresses
* only the remaining (1 - p) of the fall. */
_accelerateDrop(fx, remainingMs) {
if (!fx) return;
const now = performance.now();
if (this.fx.drop) {
this.fx.drop.duration = Math.min(this.fx.drop.duration || 120, 120);
this.fx.drop.start = Math.min(this.fx.drop.start || now, now - 90);
}
if (this.fx.heavyDrop) {
this.fx.heavyDrop.duration = Math.min(this.fx.heavyDrop.duration || 120, 120);
this.fx.heavyDrop.start = Math.min(this.fx.heavyDrop.start || now, now - 90);
}
const dur = fx.duration || 1;
const p = clamp((now - fx.start) / dur, 0, 1);
if (p >= 1) return;
const newDur = Math.max(1, remainingMs / (1 - p));
fx.duration = newDur;
fx.start = now - p * newDur;
}

requestFastStop() {
// Speed the current drop(s) up to a smooth quick landing (no teleport).
this._accelerateDrop(this.fx.drop, FAST_STOP_SETTLE_MS - 20);
this._accelerateDrop(this.fx.heavyDrop, FAST_STOP_SETTLE_MS - 20);
this.fx.cluster = null;
this.fx.sweeps = [];
this.fx.charge = null;
this.fx.spotlight = null;
this.particles = this.particles.slice(-40);
}

/** Wait `realMs` (already in real / turbo-adjusted ms — do NOT pass raw values
* through animationSleep, which would double-scale in turbo and resolve before
* the fall visually finishes, making symbols vanish mid-air). Resolves early on
* a fast-stop, but only ~FAST_STOP_SETTLE_MS after the request so the sped-up
* drop has time to visibly land first. */
_dropSettle(realMs) {
const start = performance.now();
return new Promise((resolve) => {
let stopAt = state.fastStopRequested ? start : null;
const tick = () => {
const now = performance.now();
if (stopAt === null && state.fastStopRequested) stopAt = now;
if (stopAt !== null) {
if (now - stopAt >= FAST_STOP_SETTLE_MS) return resolve();
} else if (now - start >= realMs) {
return resolve();
}
requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
});
}

cellCenter(row, col) {
const { padX, top, laneW, rowStep } = this.getLayout();
return {
Expand Down Expand Up @@ -821,7 +858,10 @@ class ReelCanvasRenderer {
const [row, col] = key.split("-").map((n) => Number(n));
return Math.max(acc, this.dropDelay(row, col, Number(count || 0)));
}, 0);
await animationSleep(duration + maxDelay + 40);
// `duration` and `maxDelay` are already turbo-adjusted, so wait the REAL time
// (not animationSleep, which re-scales and would hide the board while symbols
// are still exiting — the "half-way out then vanish" bug).
await this._dropSettle(duration + maxDelay + 40);
this.fx.drop = null;
this.board.hidden = true;
}
Expand Down Expand Up @@ -1087,7 +1127,10 @@ class ReelCanvasRenderer {

// Tail buffer: 30ms when no reveal cells (just settles the landing
// frame), 90ms when reveals are running (lets the reveal-pop finish).
await animationSleep(dropDuration + maxDelay + revealLeadMaxMs + (hasRevealCells ? 90 : 30));
// dropDuration / maxDelay are already turbo-adjusted — wait the REAL time so
// the fall fully lands before the board commits (animationSleep would double-
// scale in turbo and commit mid-fall → symbols vanish half-way down).
await this._dropSettle(dropDuration + maxDelay + revealLeadMaxMs + (hasRevealCells ? 90 : 30));
this.fx.drop = null;
this.fx.heavyDrop = null;
if (window.__renderDebug) {
Expand Down Expand Up @@ -1390,12 +1433,16 @@ class ReelCanvasRenderer {
getLayout() {
const width = this.canvas.width / this.dpr;
const height = this.canvas.height / this.dpr;
const padX = clamp(width * 0.04, 20, 40);
const top = clamp(height * 0.08, 34, 62);
const bottom = clamp(height * 0.08, 30, 60);
// Tighter frame padding so the 6×5 grid claims more of the canvas → bigger
// cells and bigger symbols (was 0.04 padX / 0.08 top+bottom, which wasted
// ~17% of the height on margins). Kept a small top band for the reel-frame
// crown/spine that draws above the first row.
const padX = clamp(width * 0.022, 8, 22);
const top = clamp(height * 0.045, 14, 34);
const bottom = clamp(height * 0.04, 12, 30);
const laneW = (width - padX * 2) / this.cols;
const rowStep = (height - top - bottom) / this.rows;
const radius = Math.min(laneW, rowStep) * 0.42;
const radius = Math.min(laneW, rowStep) * 0.46;
return { width, height, padX, top, bottom, laneW, rowStep, radius };
}

Expand Down Expand Up @@ -1443,8 +1490,17 @@ class ReelCanvasRenderer {
}

dropDelay(row, col, count) {
// Subtle per-column stagger (~25ms); per-row stagger kept light.
return Math.max(0, col * 25 + row * 10 + Math.max(0, count - 1) * 14);
// Fast-stop collapses the whole board in at once — no cascade.
if (state.fastStopRequested) return 0;
// Pronounced left-to-right REEL cascade: each column starts dropping a clear
// beat (~90ms) after the column to its left, so reel 1 lands first, then reel
// 2, then reel 3 … (Pragmatic-style waterfall) instead of the whole board
// arriving together. A small per-row stagger keeps each column reading as
// falling gravity rather than a rigid block, and symbols that fall farther
// (higher count) wait a touch longer. Scaled by turbo so the cascade
// compresses in step with the rest of the spin.
const raw = col * 90 + row * 12 + Math.max(0, count - 1) * 14;
return Math.max(0, Math.round(raw * turboScale()));
}

computeWinningBBox(winning) {
Expand Down Expand Up @@ -1999,9 +2055,11 @@ class ReelCanvasRenderer {
const coreR = radius * 1.05 * baseScale * revealScale;
const scatterSizeBoost = symbol === "SCATTER" ? 1.22 : 1;
const multiScale = symbol === "MULTI" ? multiplierIconScale(this.board.multiMap.get(key)) : 1;
const maxIconBase = Math.min(laneW, rowStep) * (symbol === "MULTI" ? 1.16 : 1.02);
// Fill the cell: symbols are drawn nearly edge-to-edge (Pragmatic-style)
// instead of the old ~0.94-of-cell size that left big gaps.
const maxIconBase = Math.min(laneW, rowStep) * (symbol === "MULTI" ? 1.24 : 1.12);
const iconBase = Math.min(
radius * 2.25 * baseScale * scatterSizeBoost * revealScale * multiScale,
radius * 2.5 * baseScale * scatterSizeBoost * revealScale * multiScale,
maxIconBase * baseScale * revealScale
);
const iconW = iconBase * squashX;
Expand Down
107 changes: 69 additions & 38 deletions client/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,51 @@ body.public-mode .reels-canvas {
height: 100% !important;
}

/* ----------------------------------------------------------------------------
Desktop / wide-landscape player view. Give the reel board the lion's share of
the vertical space and a reference-style (Pragmatic) 3:2 shape so cells are
large and slightly wider than tall — much bigger symbols than the default
full-width/short board. Scoped to desktop so the mobile stack is untouched.
---------------------------------------------------------------------------- */
@media (min-width: 1000px) and (min-height: 700px) {
/* Board sized by HEIGHT (fills the reclaimed vertical space) with a 3:2 shape;
width derives from that, capped at the container and centred. */
body.public-mode .reels-stage {
height: 100%;
width: auto;
max-width: 100%;
aspect-ratio: 3 / 2;
margin: 0 auto;
}

/* The board no longer fills the full window width, so drop the window's own
dark fill and let the temple backdrop show in the side margins around the
reel frame (the reels-stage keeps its own framed background). */
body.public-mode .vault-window {
background: transparent;
border-color: transparent;
box-shadow: none;
}

/* Compact the surrounding chrome so .vault-window (flex:1) absorbs more height. */
body.public-mode .vault-shell { gap: 6px; }
body.public-mode .hud-message-rail { min-height: 16px; }
body.public-mode .hud-bar { padding: 6px 14px; }
body.public-mode .ritual-deck { gap: 10px; padding-bottom: 0; }
body.public-mode .img-btn-spin {
max-width: 104px;
max-height: 104px;
}
body.public-mode .img-btn-buy,
body.public-mode .ante-toggle {
height: 56px;
}
body.public-mode .control-toolbar {
padding-top: 2px;
padding-bottom: 2px;
}
}

/* ----------------------------------------------------------------------------
Mobile portrait-first tuning (player view only). The public-mode layout is
already a vertical stack (title → HUD → reels → controls); here we size the
Expand Down Expand Up @@ -769,8 +814,7 @@ body.public-mode .reels-canvas {
padding-bottom: 2px;
}
body.public-mode .img-btn-buy,
body.public-mode .ante-toggle,
body.public-mode .bet-stepper {
body.public-mode .ante-toggle {
height: 56px;
}
body.public-mode .hud-node strong {
Expand Down Expand Up @@ -809,8 +853,7 @@ body.public-mode .reels-canvas {
max-height: clamp(72px, 12vh, 104px);
}
body.public-mode .img-btn-buy,
body.public-mode .ante-toggle,
body.public-mode .bet-stepper {
body.public-mode .ante-toggle {
height: 48px;
}
}
Expand Down Expand Up @@ -2034,11 +2077,15 @@ button.img-btn,
.bet-stepper {
position: relative;
display: block;
height: 64px;
/* Match the BET pill image's aspect so clickable circles align. */
aspect-ratio: 768 / 220;
max-width: 100%;
background: url("/assets/symbols/BET-removebg-preview.png") center / contain no-repeat;
/* WIDTH-driven so the box aspect ALWAYS equals the pill image's intrinsic
aspect (776×183). A fixed height + capped width would override aspect-ratio
and letterbox the pill, pushing the drawn +/- circles out of the invisible
round hit-zones (the "have to click off-target" bug). With width driving the
height, `contain` fills the box exactly and the %-positioned buttons always
land dead-on the circles. */
width: min(100%, 260px);
aspect-ratio: 776 / 183;
background: url("/assets/symbols/BET-removebg-preview.png") center / 100% 100% no-repeat;
user-select: none;
margin: 0 auto;
}
Expand All @@ -2065,8 +2112,8 @@ button.bet-step,
outline: 0;
transition: transform 0.1s ease, filter 0.12s ease;
}
.bet-step-down { left: 1.5%; }
.bet-step-up { right: 1.5%; }
.bet-step-down { left: 2%; }
.bet-step-up { right: 2%; }
.bet-step:hover { filter: brightness(1.18); }
.bet-step:active { transform: translateY(-50%) scale(0.92); }
.bet-step:disabled { cursor: not-allowed; opacity: 0.5; }
Expand Down Expand Up @@ -2747,42 +2794,26 @@ pre {

body {
font-family: "SF Pro Display", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
/* Drowned sun-temple scene is the whole-GAME backdrop: one fixed, viewport-
covering image sits behind everything so the temple fills the full view (not
just the reel panel). A soft blue top-glow + dark scrim over the image keep the
UI readable and blend it with the underwater theme; the reel table floats on top. */
background:
radial-gradient(1200px 560px at 12% -8%, rgba(130, 176, 255, 0.22), transparent 60%),
radial-gradient(1100px 520px at 92% -12%, rgba(116, 232, 255, 0.15), transparent 58%),
linear-gradient(180deg, rgba(5, 10, 20, 0.55) 0%, rgba(8, 15, 31, 0.62) 42%, rgba(13, 23, 48, 0.7) 100%),
url("assets/symbols/Background.png") center / cover no-repeat fixed,
linear-gradient(180deg, #050a14 0%, #080f1f 42%, #0d1730 100%);
/* Clean, static temple backdrop: the image is shown FULLY and clearly — no dark
scrim, no colour wash, no animated overlays. One fixed, viewport-covering image
fills the whole game; the reel table and controls float on top. */
background: url("assets/symbols/Background.png") center / cover no-repeat fixed;
background-color: #050a14;
}

body::before {
background:
radial-gradient(circle at 22% 18%, rgba(181, 213, 255, 0.06), transparent 32%),
radial-gradient(circle at 80% 20%, rgba(148, 237, 255, 0.05), transparent 30%),
linear-gradient(90deg, transparent 0, rgba(200, 225, 255, 0.02) 50%, transparent 100%),
repeating-linear-gradient(90deg, rgba(198, 221, 255, 0.012) 0px, rgba(198, 221, 255, 0.012) 1px, transparent 1px, transparent 42px);
}

/* Overlay layers removed — nothing washes/tints the background anymore. */
body::before,
body::after {
background: linear-gradient(180deg, rgba(205, 230, 255, 0.04), transparent 14%, transparent 84%, rgba(3, 7, 16, 0.34));
display: none;
}

.altar {
border-radius: 20px;
border-color: rgba(171, 207, 255, 0.26);
/* The temple scene is painted on the panel itself too (under a translucent tint
for readability), so the altar carries the backdrop directly rather than only
letting the <body> image show through. `cover` fills the panel responsively. */
background:
linear-gradient(180deg, rgba(14, 23, 42, 0.38), rgba(7, 13, 28, 0.44)),
url("assets/symbols/Background.png") center / cover no-repeat;
backdrop-filter: blur(20px) saturate(140%);
-webkit-backdrop-filter: blur(20px) saturate(140%);
/* Transparent so the clean <body> temple image shows through fully. No own image
copy, no dark scrim, and NO backdrop-filter blur (that frosted/blurred the
background — the "blended, not fully shown" look). */
background: transparent;
}

.status-ribbon-item,
Expand Down
Loading