new: usr: add initialization bonus points' system to the gameplay #5
This commit is contained in:
25
assets/js/mine-seeker/components/BonusBox.jsx
Normal file
25
assets/js/mine-seeker/components/BonusBox.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const BonusBox = ({ color, points, onClick, title }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={`bonus-box ${color}-bonus`}
|
||||
onClick={onClick}
|
||||
title={title || 'View bonus statistics'}
|
||||
aria-label={`${color} bonus points: ${points}`}
|
||||
>
|
||||
<i className="fa fa-star bonus-box__icon" />
|
||||
<span className="bonus-box__value">{points}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
export default BonusBox;
|
||||
97
assets/js/mine-seeker/components/BonusStatsDialog.jsx
Normal file
97
assets/js/mine-seeker/components/BonusStatsDialog.jsx
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* This file is part of the SplendidBear Websites' projects.
|
||||
*
|
||||
* Copyright (c) 2026 @ www.splendidbear.org
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import { BONUS_LABELS } from '@mine-utils';
|
||||
|
||||
const DIALOG_SX = {
|
||||
'& .MuiDialog-paper': {
|
||||
background: '#07090d',
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(35, 111, 135, 0.08) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(35, 111, 135, 0.08) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '46px 46px',
|
||||
border: '1px solid rgba(35, 111, 135, 0.4)',
|
||||
borderRadius: '12px',
|
||||
boxShadow: '0 0 80px rgba(35, 111, 135, 0.15), 0 32px 80px rgba(0, 0, 0, 0.9)',
|
||||
width: '560px',
|
||||
maxWidth: '94vw',
|
||||
overflow: 'hidden',
|
||||
color: '#fff',
|
||||
},
|
||||
'& .MuiBackdrop-root': {
|
||||
background: 'rgba(2, 4, 8, 0.88)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
},
|
||||
};
|
||||
|
||||
const formatPlayerName = name => {
|
||||
if (name && name.startsWith('anon_')) {
|
||||
return 'Anonymous';
|
||||
}
|
||||
|
||||
if (name && 10 < name.length) {
|
||||
return name.substring(0, 7) + '...';
|
||||
}
|
||||
|
||||
return name || 'Unknown';
|
||||
};
|
||||
|
||||
const PlayerColumn = ({ color, player }) => (
|
||||
<div className={`bsd-column bsd-column--${color}`}>
|
||||
<div className="bsd-column-header">
|
||||
<span className="bsd-column-name">{formatPlayerName(player.name)}</span>
|
||||
<span className="bsd-column-total">
|
||||
<i className="fa fa-star" />
|
||||
{player.bonusPoints}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="bsd-stats">
|
||||
{Object.entries(BONUS_LABELS).map(([key, { label, desc }]) => (
|
||||
<li key={key} className="bsd-stat">
|
||||
<div className="bsd-stat-text">
|
||||
<span className="bsd-stat-label">{label}</span>
|
||||
<span className="bsd-stat-desc">{desc}</span>
|
||||
</div>
|
||||
<span className="bsd-stat-value">{player.bonusStats?.[key] ?? 0}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
const BonusStatsDialog = ({ open, onClose, red, blue }) => (
|
||||
<Dialog open={open} onClose={onClose} sx={DIALOG_SX}>
|
||||
<div className="bsd">
|
||||
<div className="bsd-header">
|
||||
<div className="bsd-header-text">
|
||||
<span className="bsd-label">Scoring</span>
|
||||
<h2 className="bsd-title">
|
||||
<i className="fa fa-star" />
|
||||
Bonus Statistics
|
||||
</h2>
|
||||
</div>
|
||||
<button className="bsd-close" onClick={onClose} aria-label="Close">
|
||||
<i className="fa fa-times" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="bsd-body">
|
||||
<PlayerColumn color="red" player={red} />
|
||||
<PlayerColumn color="blue" player={blue} />
|
||||
</div>
|
||||
<p className="bsd-note">
|
||||
Bonus points are awarded alongside the main score for skillful play.
|
||||
</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
export default BonusStatsDialog;
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useGame } from '@mine-contexts';
|
||||
import BonusBox from './BonusBox';
|
||||
import BonusStatsDialog from './BonusStatsDialog';
|
||||
|
||||
const renderAvatar = player => {
|
||||
if (!player.registered) return null;
|
||||
@@ -27,6 +29,7 @@ const GameTimer = () => {
|
||||
const [redTime, setRedTime] = useState(0);
|
||||
const [blueTime, setBlueTime] = useState(0);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
|
||||
const timerIntervalRef = useRef(null);
|
||||
const gameStartedRef = useRef(false);
|
||||
|
||||
@@ -160,8 +163,12 @@ const GameTimer = () => {
|
||||
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const openBonusDialog = () => setBonusDialogOpen(true);
|
||||
const closeBonusDialog = () => setBonusDialogOpen(false);
|
||||
|
||||
return (
|
||||
<div className="game-timer-container">
|
||||
<BonusBox color="red" points={red.bonusPoints ?? 0} onClick={openBonusDialog} />
|
||||
<div className={`game-timer red-timer ${!activePlayer ? 'active' : ''}`}>
|
||||
{renderAvatar(red)}
|
||||
<i className={`fa ${!activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||
@@ -172,6 +179,8 @@ const GameTimer = () => {
|
||||
<i className={`fa ${activePlayer ? 'fa-hourglass-half' : 'fa-hourglass-start'} timer-icon`} />
|
||||
<span className="timer-display">{formatTime(blueTime)}</span>
|
||||
</div>
|
||||
<BonusBox color="blue" points={blue.bonusPoints ?? 0} onClick={openBonusDialog} />
|
||||
<BonusStatsDialog open={bonusDialogOpen} onClose={closeBonusDialog} red={red} blue={blue} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { useGame } from '@mine-contexts';
|
||||
import User from './User';
|
||||
import BonusStatsDialog from '../BonusStatsDialog';
|
||||
|
||||
const UserControl = ({ resign }) => {
|
||||
const { webPlayer, activePlayer, mines, foundMines, red, blue, onBombToggle } = useGame();
|
||||
const [bonusDialogOpen, setBonusDialogOpen] = useState(false);
|
||||
const activeColor = activePlayer ? 'blue' : 'red';
|
||||
const resignClass = 'resign' + (activeColor !== webPlayer ? ' disabled' : '');
|
||||
const minesClass = 'active-mines' + (foundMines ? ' found-mine' : '');
|
||||
@@ -24,30 +26,44 @@ const UserControl = ({ resign }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleBonusClick = () => {
|
||||
setBonusDialogOpen(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="users">
|
||||
<User
|
||||
color="blue" webPlayer={webPlayer} {...blue}
|
||||
onClickBombSelector={() => handleBombClick('blue', 1)}
|
||||
/>
|
||||
<div className="active-mines-container">
|
||||
<i className="fa fa-star" />
|
||||
<div className={minesClass}>
|
||||
<div className="active-mines-nbr">{mines}</div>
|
||||
<div className="active-mines-shine" />
|
||||
<Fragment>
|
||||
<div className="users">
|
||||
<User
|
||||
color="blue" webPlayer={webPlayer} {...blue}
|
||||
onClickBombSelector={() => handleBombClick('blue', 1)}
|
||||
onBonusClick={handleBonusClick}
|
||||
/>
|
||||
<div className="active-mines-container">
|
||||
<i className="fa fa-star" />
|
||||
<div className={minesClass}>
|
||||
<div className="active-mines-nbr">{mines}</div>
|
||||
<div className="active-mines-shine" />
|
||||
</div>
|
||||
<i className="fa fa-star" />
|
||||
</div>
|
||||
<i className="fa fa-star" />
|
||||
<div className="clear" />
|
||||
<User
|
||||
color="red" webPlayer={webPlayer} {...red}
|
||||
onClickBombSelector={() => handleBombClick('red', 0)}
|
||||
onBonusClick={handleBonusClick}
|
||||
/>
|
||||
<button className={resignClass} onClick={resign}>
|
||||
<div className="resign-shine" />
|
||||
Resign
|
||||
</button>
|
||||
</div>
|
||||
<div className="clear" />
|
||||
<User
|
||||
color="red" webPlayer={webPlayer} {...red}
|
||||
onClickBombSelector={() => handleBombClick('red', 0)}
|
||||
<BonusStatsDialog
|
||||
open={bonusDialogOpen}
|
||||
onClose={() => setBonusDialogOpen(false)}
|
||||
red={red}
|
||||
blue={blue}
|
||||
/>
|
||||
<button className={resignClass} onClick={resign}>
|
||||
<div className="resign-shine" />
|
||||
Resign
|
||||
</button>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,18 @@ export const GameProvider = ({ children }) => {
|
||||
};
|
||||
|
||||
const applyStep = stepData => {
|
||||
const { player, bomb: isBomb, minesFound = 0, revealedCells = [], redPoints: rp, bluePoints: bp } = stepData;
|
||||
const {
|
||||
player,
|
||||
bomb: isBomb,
|
||||
minesFound = 0,
|
||||
revealedCells = [],
|
||||
redPoints: rp,
|
||||
bluePoints: bp,
|
||||
redBonusPoints = 0,
|
||||
blueBonusPoints = 0,
|
||||
redBonusStats = {},
|
||||
blueBonusStats = {},
|
||||
} = stepData;
|
||||
|
||||
if (isBomb) {
|
||||
sounds.current.bomb.play();
|
||||
@@ -176,6 +187,18 @@ export const GameProvider = ({ children }) => {
|
||||
syncBlue(p => ({ ...p, mines: 'blue' === player ? bp : p.mines }));
|
||||
}
|
||||
|
||||
// Update bonus points and stats
|
||||
syncRed(p => ({
|
||||
...p,
|
||||
bonusPoints: 'red' === player ? redBonusPoints : p.bonusPoints,
|
||||
bonusStats: 'red' === player ? redBonusStats : p.bonusStats,
|
||||
}));
|
||||
syncBlue(p => ({
|
||||
...p,
|
||||
bonusPoints: 'blue' === player ? blueBonusPoints : p.bonusPoints,
|
||||
bonusStats: 'blue' === player ? blueBonusStats : p.bonusStats,
|
||||
}));
|
||||
|
||||
syncRed(p => ({ ...p, enabledBomb: rp <= bp }));
|
||||
syncBlue(p => ({ ...p, enabledBomb: bp <= rp }));
|
||||
|
||||
|
||||
@@ -34,9 +34,23 @@ export const IMAGES = {
|
||||
bombPos: (hor, vert) => `${IMG}bg-bomb-${hor}-${vert}-outbg.png`,
|
||||
};
|
||||
|
||||
export const BONUS_STATS_DEF = {
|
||||
blindHits: 0, chainBest: 0, chainCurrent: 0, lastMineHits: 0, edgeMines: 0, biggestReveal: 0,
|
||||
};
|
||||
|
||||
export const BONUS_LABELS = {
|
||||
blindHits: { label: 'Blind hits', desc: 'Mines clicked with no revealed number nearby' },
|
||||
chainBest: { label: 'Best chain', desc: 'Longest streak of consecutive mine-clicks' },
|
||||
chainCurrent: { label: 'Current chain', desc: 'Active consecutive mine-click streak' },
|
||||
lastMineHits: { label: 'Endgame mines', desc: 'Mines clicked while few remain on the board' },
|
||||
edgeMines: { label: 'Edge mines', desc: 'Mines clicked on the board boundary' },
|
||||
biggestReveal: { label: 'Biggest reveal', desc: 'Largest number of safe cells revealed in one click' },
|
||||
};
|
||||
|
||||
export const PLAYER_DEF = {
|
||||
name: '...', desc: '', active: false, mines: 0, haveBomb: true, enabledBomb: true,
|
||||
registered: false, avatar: null,
|
||||
bonusPoints: 0, bonusStats: { ...BONUS_STATS_DEF },
|
||||
};
|
||||
|
||||
export const DESC = {
|
||||
|
||||
@@ -7,4 +7,4 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
export { DESC, IMAGES, BOMB_SYMBOLS, PLAYER_DEF, bombRadius, initCells, patchCells } from './constants';
|
||||
export { DESC, IMAGES, BOMB_SYMBOLS, PLAYER_DEF, BONUS_STATS_DEF, BONUS_LABELS, bombRadius, initCells, patchCells } from './constants';
|
||||
|
||||
Reference in New Issue
Block a user