chg: dev: more, massive refactor for front-end #4
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { GameProvider } from './contexts/GameContext';
|
import { GameProvider } from './contexts/GameProvider';
|
||||||
import { GameBoard } from './components/GameBoard';
|
import { GameBoard } from './components/GameBoard';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useGame } from '../contexts/GameContext';
|
import { useGame } from '../contexts/GameContext';
|
||||||
import useServerComm from '../hooks/useServerComm';
|
import useServerCommunication from '../hooks/useServerCommunication';
|
||||||
import GridControl from './grid/GridControl';
|
import GridControl from './grid/GridControl';
|
||||||
|
|
||||||
export const GameBoard = ({ gameAssoc, gameInherited, isEnvDev }) => {
|
export const GameBoard = ({ gameAssoc, gameInherited, isEnvDev }) => {
|
||||||
const { gridReady } = useGame();
|
const { gridReady } = useGame();
|
||||||
const { onClick, resign } = useServerComm(gameAssoc, gameInherited, isEnvDev);
|
const { onClick, resign } = useServerCommunication(gameAssoc, gameInherited, isEnvDev);
|
||||||
|
|
||||||
if (!gridReady) {
|
if (!gridReady) {
|
||||||
return (
|
return (
|
||||||
@@ -24,5 +24,10 @@ export const GameBoard = ({ gameAssoc, gameInherited, isEnvDev }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <GridControl onClick={onClick} resign={resign} />;
|
return (
|
||||||
|
<GridControl
|
||||||
|
onClick={onClick}
|
||||||
|
resign={resign}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import React from 'react';
|
|||||||
import { useGame } from '../../contexts/GameContext';
|
import { useGame } from '../../contexts/GameContext';
|
||||||
import GridField from './GridField';
|
import GridField from './GridField';
|
||||||
import UserControl from '../user/UserControl';
|
import UserControl from '../user/UserControl';
|
||||||
import { BOMB_SYMBOLS, bombRadius } from '../../constants';
|
import { BOMB_SYMBOLS, bombRadius } from '../../utils/constants';
|
||||||
|
|
||||||
const GridControl = ({ onClick, resign }) => {
|
const GridControl = ({ onClick, resign }) => {
|
||||||
const {
|
const {
|
||||||
overlay, overlayTitle, overlaySubTitle,
|
overlay, overlayTitle, overlaySubTitle,
|
||||||
webPlayer, activePlayer, mines, foundMines, bombSelected,
|
webPlayer, activePlayer, bombSelected,
|
||||||
red, blue, cells, setCells, onBombToggle,
|
cells, setCells,
|
||||||
} = useGame();
|
} = useGame();
|
||||||
|
|
||||||
const handleHover = (row, col) => {
|
const handleHover = (row, col) => {
|
||||||
@@ -39,13 +39,6 @@ const GridControl = ({ onClick, resign }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UserControl
|
<UserControl
|
||||||
webPlayer={webPlayer}
|
|
||||||
activePlayer={activePlayer}
|
|
||||||
mines={mines}
|
|
||||||
foundMines={foundMines}
|
|
||||||
red={red}
|
|
||||||
blue={blue}
|
|
||||||
onBombToggle={onBombToggle}
|
|
||||||
resign={resign}
|
resign={resign}
|
||||||
/>
|
/>
|
||||||
<div className="grid-container">
|
<div className="grid-container">
|
||||||
@@ -58,7 +51,7 @@ const GridControl = ({ onClick, resign }) => {
|
|||||||
onClick={() => onClick([r, c])}
|
onClick={() => onClick([r, c])}
|
||||||
onMouseEnter={() => handleHover(r, c)}
|
onMouseEnter={() => handleHover(r, c)}
|
||||||
/>
|
/>
|
||||||
))
|
)),
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { IMG } from '../../constants';
|
import { IMAGES } from '../../utils/constants';
|
||||||
|
|
||||||
const bombSrc = area => {
|
const bombSrc = area => {
|
||||||
if (null === area) return null;
|
if (null === area) return null;
|
||||||
const vert = ['left', 'center', 'right'][area[0]] ?? null;
|
const vert = ['left', 'center', 'right'][area[0]] ?? null;
|
||||||
const hor = ['top', 'middle', 'bottom'][area[1]] ?? null;
|
const hor = ['top', 'middle', 'bottom'][area[1]] ?? null;
|
||||||
if (null === vert || null === hor) return IMG + 'bg-bomb-empty-outbg.png';
|
if (null === vert || null === hor) return IMAGES.bombEmpty;
|
||||||
return `${IMG}bg-bomb-${hor}-${vert}-outbg.png`;
|
return IMAGES.bombPos(hor, vert);
|
||||||
};
|
};
|
||||||
|
|
||||||
const GridField = memo(function GridField({ cell, onClick, onMouseEnter }) {
|
const GridField = memo(function GridField({ cell, onClick, onMouseEnter }) {
|
||||||
@@ -28,13 +28,12 @@ const GridField = memo(function GridField({ cell, onClick, onMouseEnter }) {
|
|||||||
const bSrc = bombSrc(bombTargetArea);
|
const bSrc = bombSrc(bombTargetArea);
|
||||||
const showLast = lastClickedRed || lastClickedBlue;
|
const showLast = lastClickedRed || lastClickedBlue;
|
||||||
const lastClass = 'field-' + (lastClickedRed ? 'red' : 'blue') + '-last last-clicked';
|
const lastClass = 'field-' + (lastClickedRed ? 'red' : 'blue') + '-last last-clicked';
|
||||||
const lastSrc = lastClickedRed ? IMG + 'bg-last-red-outbg.png' : IMG + 'bg-last-blue-outbg.png';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="field-wrapper" onClick={onClick} onMouseEnter={onMouseEnter}>
|
<div className="field-wrapper" onClick={onClick} onMouseEnter={onMouseEnter}>
|
||||||
<img className="field-target" src={IMG + 'bg-target-outbg.png'} alt="" />
|
<img className="field-target" src={IMAGES.target} alt="" />
|
||||||
{bSrc && <img className="field-bomb-target" src={bSrc} alt="" />}
|
{bSrc && <img className="field-bomb-target" src={bSrc} alt="" />}
|
||||||
{showLast && <img className={lastClass} src={lastSrc} alt="" />}
|
{showLast && <img className={lastClass} src={IMAGES.last(lastClickedRed ? 'red' : 'blue')} alt="" />}
|
||||||
<div className={fieldClass}>
|
<div className={fieldClass}>
|
||||||
<div className="field-corner">{inner}</div>
|
<div className="field-corner">{inner}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,34 +1,38 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
|
import { IMAGES } from '../../utils/constants';
|
||||||
const SRC = '/images/';
|
|
||||||
|
|
||||||
const User = memo(function User({
|
const User = memo(function User({
|
||||||
color, webPlayer,
|
color,
|
||||||
name, desc, active, mines, haveBomb, enabledBomb,
|
webPlayer,
|
||||||
|
name,
|
||||||
|
desc,
|
||||||
|
active,
|
||||||
|
mines,
|
||||||
|
haveBomb,
|
||||||
|
enabledBomb,
|
||||||
onClickBombSelector,
|
onClickBombSelector,
|
||||||
}) {
|
}) {
|
||||||
const buzzClass = 'bomb-container'
|
const buzzClass = 'bomb-container'
|
||||||
+ (active && color === webPlayer && haveBomb && enabledBomb ? ' buzz' : '');
|
+ (active && color === webPlayer && haveBomb && enabledBomb ? ' buzz' : '');
|
||||||
|
|
||||||
const bombImg = haveBomb
|
|
||||||
? SRC + (enabledBomb && active ? 'bg-bomb-outbg.png' : 'bg-bomb-disabled-outbg.png')
|
|
||||||
: SRC + 'bg-bomb-exploded-outbg.png';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`user-container user-${color}`}>
|
<div className={`user-container user-${color}`}>
|
||||||
<div className="user-header">
|
<div className="user-header">
|
||||||
<div className="user-color">{color}</div>
|
<div className="user-color">{color}</div>
|
||||||
{active && <img src={`${SRC}bg-cursor-${color}-outbg.png`} alt="" className="user-cursor" />}
|
{active && <img src={IMAGES.cursor(color)} alt="" className="user-cursor" />}
|
||||||
<img src={`${SRC}bg-figure-${color}-outbg.png`} alt="" />
|
<img src={IMAGES.figure(color)} alt="" />
|
||||||
</div>
|
</div>
|
||||||
<div className="user-name"> {name} </div>
|
<div className="user-name"> {name} </div>
|
||||||
<div className="user-caret"><i className="fa fa-caret-down" /></div>
|
<div className="user-caret"><i className="fa fa-caret-down" /></div>
|
||||||
<div className="user-desc"> {desc} </div>
|
<div className="user-desc"> {desc} </div>
|
||||||
<div className="user-control">
|
<div className="user-control">
|
||||||
<img src={`${SRC}bg-flag-${color}-outbg.png`} alt="" />
|
<img src={IMAGES.flag(color)} alt="" />
|
||||||
<div className="user-control-mines">{mines}</div>
|
<div className="user-control-mines">{mines}</div>
|
||||||
<div className={buzzClass} onClick={onClickBombSelector}>
|
<div className={buzzClass} onClick={onClickBombSelector}>
|
||||||
<div className="bomb"><img src={bombImg} alt="" /></div>
|
<div className="bomb">
|
||||||
|
{haveBomb && <img src={enabledBomb && active ? IMAGES.bomb : IMAGES.bombDisabled} alt="" />}
|
||||||
|
{!haveBomb && <img src={IMAGES.bombExploded} alt="" />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="clear" />
|
<div className="clear" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useGame } from '../../contexts/GameContext';
|
||||||
import User from './User';
|
import User from './User';
|
||||||
|
|
||||||
const UserControl = ({ webPlayer, activePlayer, mines, foundMines, red, blue, onBombToggle, resign }) => {
|
const UserControl = ({ resign }) => {
|
||||||
|
const { webPlayer, activePlayer, mines, foundMines, red, blue, onBombToggle } = useGame();
|
||||||
const activeColor = activePlayer ? 'blue' : 'red';
|
const activeColor = activePlayer ? 'blue' : 'red';
|
||||||
const resignClass = 'resign' + (activeColor !== webPlayer ? ' disabled' : '');
|
const resignClass = 'resign' + (activeColor !== webPlayer ? ' disabled' : '');
|
||||||
const minesClass = 'active-mines' + (foundMines ? ' found-mine' : '');
|
const minesClass = 'active-mines' + (foundMines ? ' found-mine' : '');
|
||||||
|
|||||||
@@ -1,233 +1,7 @@
|
|||||||
import React, { createContext, useContext, useRef, useState } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import { Howl } from 'howler';
|
|
||||||
import { IMG, PLAYER_DEF, DESC, patchCells, initCells } from '../constants';
|
|
||||||
|
|
||||||
// ── Context ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const GameContext = createContext(null);
|
const GameContext = createContext(null);
|
||||||
|
|
||||||
export const useGame = () => useContext(GameContext);
|
export const useGame = () => useContext(GameContext);
|
||||||
|
|
||||||
// ── Provider ─────────────────────────────────────────────────────────────────
|
export default GameContext;
|
||||||
|
|
||||||
export const GameProvider = ({ children }) => {
|
|
||||||
// Refs that are read inside async callbacks (stay in sync with state via sync* helpers)
|
|
||||||
const webPlayerRef = useRef(null);
|
|
||||||
const activePlayerRef = useRef(false);
|
|
||||||
const bombSelectedRef = useRef(false);
|
|
||||||
const connectionLostRef = useRef(false);
|
|
||||||
const redRef = useRef({ ...PLAYER_DEF });
|
|
||||||
const blueRef = useRef({ ...PLAYER_DEF });
|
|
||||||
const lastClickedRef = useRef({ red: null, blue: null });
|
|
||||||
const endRef = useRef(false);
|
|
||||||
|
|
||||||
const sounds = useRef({
|
|
||||||
click: new Howl({ src: ['/sound/click.mp3'] }),
|
|
||||||
bomb: new Howl({ src: ['/sound/bomb.mp3'] }),
|
|
||||||
mine: new Howl({ src: ['/sound/mine.mp3'] }),
|
|
||||||
warning: new Howl({ src: ['/sound/warning.mp3'] }),
|
|
||||||
won: new Howl({ src: ['/sound/won.mp3'] }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Display state
|
|
||||||
const [webPlayer, setWebPlayer] = useState(null);
|
|
||||||
const [activePlayer, setActivePlayer] = useState(false);
|
|
||||||
const [overlay, setOverlay] = useState(true);
|
|
||||||
const [overlayTitle, setOverlayTitle] = useState('');
|
|
||||||
const [overlaySubTitle, setOverlaySubTitle] = useState('');
|
|
||||||
const [mines, setMines] = useState(51);
|
|
||||||
const [bombSelected, setBombSelected] = useState(false);
|
|
||||||
const [foundMines, setFoundMines] = useState(false);
|
|
||||||
const [red, setRed] = useState({ ...PLAYER_DEF });
|
|
||||||
const [blue, setBlue] = useState({ ...PLAYER_DEF });
|
|
||||||
const [cells, setCells] = useState(initCells);
|
|
||||||
const [gridReady, setGridReady] = useState(false);
|
|
||||||
const [connectionLost, setConnectionLost] = useState(false);
|
|
||||||
|
|
||||||
// ── Sync helpers (keep ref + state in lockstep) ──────────────────────────
|
|
||||||
const syncWebPlayer = v => {
|
|
||||||
webPlayerRef.current = v;
|
|
||||||
setWebPlayer(v);
|
|
||||||
};
|
|
||||||
const syncActivePlayer = v => {
|
|
||||||
activePlayerRef.current = v;
|
|
||||||
setActivePlayer(v);
|
|
||||||
};
|
|
||||||
const syncBombSelected = v => {
|
|
||||||
bombSelectedRef.current = v;
|
|
||||||
setBombSelected(v);
|
|
||||||
};
|
|
||||||
const syncConnLost = v => {
|
|
||||||
connectionLostRef.current = v;
|
|
||||||
setConnectionLost(v);
|
|
||||||
};
|
|
||||||
const syncRed = fn => {
|
|
||||||
const n = fn(redRef.current);
|
|
||||||
redRef.current = n;
|
|
||||||
setRed(n);
|
|
||||||
};
|
|
||||||
const syncBlue = fn => {
|
|
||||||
const n = fn(blueRef.current);
|
|
||||||
blueRef.current = n;
|
|
||||||
setBlue(n);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Overlay ───────────────────────────────────────────────────────────────
|
|
||||||
const showOverlay = (title, sub) => {
|
|
||||||
setOverlay(true);
|
|
||||||
setOverlayTitle(title);
|
|
||||||
setOverlaySubTitle(sub);
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideOverlay = () => setOverlay(false);
|
|
||||||
|
|
||||||
// ── Cell helpers ──────────────────────────────────────────────────────────
|
|
||||||
const applyRevealedCell = (cell, player, isMainCell = false) => {
|
|
||||||
const { row, col, value } = cell;
|
|
||||||
setCells(prev => {
|
|
||||||
if (prev[row][col].active) return prev;
|
|
||||||
const patch = 'm' === value
|
|
||||||
? { currentImage: `${IMG}bg-flag-${player}-outbg.png`, currentObj: 'm', active: true }
|
|
||||||
: { currentImage: value, currentObj: value, active: true };
|
|
||||||
if (isMainCell) {
|
|
||||||
patch.lastClickedRed = 'red' === player;
|
|
||||||
patch.lastClickedBlue = 'blue' === player;
|
|
||||||
}
|
|
||||||
return patchCells(prev, [{ row, col, ...patch }]);
|
|
||||||
});
|
|
||||||
if (isMainCell) lastClickedRef.current = { ...lastClickedRef.current, [player]: [row, col] };
|
|
||||||
};
|
|
||||||
|
|
||||||
const showLeftMines = (leftMines = []) => {
|
|
||||||
if (!leftMines.length) return;
|
|
||||||
setCells(prev => {
|
|
||||||
const patches = leftMines
|
|
||||||
.filter(({ row, col }) => !prev[row][col].active)
|
|
||||||
.map(({ row, col }) => ({ row, col, currentImage: IMG + 'bg-left-mine-outbg.png' }));
|
|
||||||
return patches.length ? patchCells(prev, patches) : prev;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Game logic ────────────────────────────────────────────────────────────
|
|
||||||
const changePlayer = () => {
|
|
||||||
const wasColor = activePlayerRef.current ? 'blue' : 'red';
|
|
||||||
const nextColor = activePlayerRef.current ? 'red' : 'blue';
|
|
||||||
const nextVal = activePlayerRef.current ? 0 : 1;
|
|
||||||
const desc = wasColor === webPlayerRef.current ? DESC.buddy : DESC.you;
|
|
||||||
|
|
||||||
syncActivePlayer(nextVal);
|
|
||||||
syncRed(p => ({ ...p, active: 'red' === nextColor, desc: 'red' === nextColor ? desc : '' }));
|
|
||||||
syncBlue(p => ({ ...p, active: 'blue' === nextColor, desc: 'blue' === nextColor ? desc : '' }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyStep = stepData => {
|
|
||||||
const { player, bomb: isBomb, minesFound = 0, revealedCells = [], redPoints: rp, bluePoints: bp } = stepData;
|
|
||||||
|
|
||||||
if (isBomb) {
|
|
||||||
sounds.current.bomb.play();
|
|
||||||
} else if (0 < minesFound) {
|
|
||||||
const cur = ('red' === player ? redRef : blueRef).current.mines;
|
|
||||||
sounds.current[20 < cur + minesFound ? 'warning' : 'mine'].play();
|
|
||||||
} else {
|
|
||||||
sounds.current.click.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
const lc = lastClickedRef.current[player];
|
|
||||||
setCells(prev => {
|
|
||||||
let next = prev;
|
|
||||||
if (lc) next = patchCells(next, [{ row: lc[0], col: lc[1], lastClickedRed: false, lastClickedBlue: false }]);
|
|
||||||
|
|
||||||
revealedCells.forEach(({ row, col, value }, idx) => {
|
|
||||||
if (next[row][col].active) return;
|
|
||||||
const patch = 'm' === value
|
|
||||||
? { currentImage: `${IMG}bg-flag-${player}-outbg.png`, currentObj: 'm', active: true }
|
|
||||||
: { currentImage: value, currentObj: value, active: true };
|
|
||||||
if (0 === idx) {
|
|
||||||
patch.lastClickedRed = 'red' === player;
|
|
||||||
patch.lastClickedBlue = 'blue' === player;
|
|
||||||
}
|
|
||||||
next = patchCells(next, [{ row, col, ...patch }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isBomb) next = next.map(r => r.map(c => null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c));
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (0 < revealedCells.length) {
|
|
||||||
lastClickedRef.current = { ...lastClickedRef.current, [player]: [revealedCells[0].row, revealedCells[0].col] };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (0 < minesFound) {
|
|
||||||
setMines(51 - rp - bp);
|
|
||||||
setFoundMines(true);
|
|
||||||
setTimeout(() => setFoundMines(false), 500);
|
|
||||||
syncRed(p => ({ ...p, mines: 'red' === player ? rp : p.mines }));
|
|
||||||
syncBlue(p => ({ ...p, mines: 'blue' === player ? bp : p.mines }));
|
|
||||||
}
|
|
||||||
|
|
||||||
syncRed(p => ({ ...p, enabledBomb: rp <= bp }));
|
|
||||||
syncBlue(p => ({ ...p, enabledBomb: bp <= rp }));
|
|
||||||
|
|
||||||
if (isBomb || 0 === minesFound) changePlayer();
|
|
||||||
|
|
||||||
if (isBomb) {
|
|
||||||
syncBombSelected(false);
|
|
||||||
syncRed(p => 'red' === player ? { ...p, haveBomb: false } : p);
|
|
||||||
syncBlue(p => 'blue' === player ? { ...p, haveBomb: false } : p);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeGameEndIfItEnds = (bluePoints, redPoints, resign = false, leftMines = []) => {
|
|
||||||
const redWins = 25 < redPoints;
|
|
||||||
const blueWins = 25 < bluePoints;
|
|
||||||
|
|
||||||
if (redWins || blueWins || resign) {
|
|
||||||
sounds.current.won.play();
|
|
||||||
|
|
||||||
if (!resign) showOverlay((redWins ? 'Red' : 'Blue') + ' wins the game!', 'Play again!');
|
|
||||||
|
|
||||||
showLeftMines(leftMines);
|
|
||||||
syncActivePlayer(false);
|
|
||||||
syncRed(p => ({ ...p, desc: '' }));
|
|
||||||
syncBlue(p => ({ ...p, desc: '' }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const resignProcess = color => {
|
|
||||||
const wp = webPlayerRef.current;
|
|
||||||
showOverlay(
|
|
||||||
color === wp ? 'You have been give up' : 'Your opponent has been resigned',
|
|
||||||
color === wp ? 'You LOSE!' : 'You WIN!',
|
|
||||||
);
|
|
||||||
endRef.current = true;
|
|
||||||
makeGameEndIfItEnds(0, 0, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onBombToggle = () => {
|
|
||||||
const next = !bombSelectedRef.current;
|
|
||||||
syncBombSelected(next);
|
|
||||||
if (!next) {
|
|
||||||
setCells(prev => prev.map(r => r.map(c => null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Context value ─────────────────────────────────────────────────────────
|
|
||||||
const value = {
|
|
||||||
// State (for rendering)
|
|
||||||
webPlayer, activePlayer, overlay, overlayTitle, overlaySubTitle,
|
|
||||||
mines, bombSelected, foundMines, red, blue, cells, gridReady, connectionLost,
|
|
||||||
// Refs (needed by useServerComm for async-safe reads)
|
|
||||||
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, endRef,
|
|
||||||
// Setters needed by useServerComm
|
|
||||||
setCells, setGridReady,
|
|
||||||
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
|
||||||
// Game logic called by useServerComm
|
|
||||||
showOverlay, hideOverlay,
|
|
||||||
applyRevealedCell, applyStep,
|
|
||||||
makeGameEndIfItEnds, resignProcess,
|
|
||||||
// UI action (bomb toggle is pure state, no server call)
|
|
||||||
onBombToggle,
|
|
||||||
};
|
|
||||||
|
|
||||||
return <GameContext.Provider value={value}>{children}</GameContext.Provider>;
|
|
||||||
};
|
|
||||||
|
|||||||
234
assets/js/mine-seeker/contexts/GameProvider.jsx
Normal file
234
assets/js/mine-seeker/contexts/GameProvider.jsx
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import { Howl } from 'howler';
|
||||||
|
import GameContext from './GameContext';
|
||||||
|
import useGameRefs from '../hooks/useGameRefs';
|
||||||
|
import useGameState from '../hooks/useGameState';
|
||||||
|
import { DESC, IMAGES, patchCells } from '../utils/constants';
|
||||||
|
|
||||||
|
export const GameProvider = ({ children }) => {
|
||||||
|
const {
|
||||||
|
webPlayerRef,
|
||||||
|
activePlayerRef,
|
||||||
|
bombSelectedRef,
|
||||||
|
connectionLostRef,
|
||||||
|
redRef,
|
||||||
|
blueRef,
|
||||||
|
lastClickedRef,
|
||||||
|
endRef,
|
||||||
|
} = useGameRefs();
|
||||||
|
|
||||||
|
const {
|
||||||
|
webPlayer, setWebPlayer,
|
||||||
|
activePlayer, setActivePlayer,
|
||||||
|
overlay, setOverlay,
|
||||||
|
overlayTitle, setOverlayTitle,
|
||||||
|
overlaySubTitle, setOverlaySubTitle,
|
||||||
|
mines, setMines,
|
||||||
|
bombSelected, setBombSelected,
|
||||||
|
foundMines, setFoundMines,
|
||||||
|
red, setRed,
|
||||||
|
blue, setBlue,
|
||||||
|
cells, setCells,
|
||||||
|
gridReady, setGridReady,
|
||||||
|
connectionLost, setConnectionLost,
|
||||||
|
} = useGameState();
|
||||||
|
|
||||||
|
const sounds = useRef({
|
||||||
|
click: new Howl({ src: ['/sound/click.mp3'] }),
|
||||||
|
bomb: new Howl({ src: ['/sound/bomb.mp3'] }),
|
||||||
|
mine: new Howl({ src: ['/sound/mine.mp3'] }),
|
||||||
|
warning: new Howl({ src: ['/sound/warning.mp3'] }),
|
||||||
|
won: new Howl({ src: ['/sound/won.mp3'] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Sync helpers (keep ref + state in lockstep) ──────────────────────────
|
||||||
|
const syncWebPlayer = v => {
|
||||||
|
webPlayerRef.current = v;
|
||||||
|
setWebPlayer(v);
|
||||||
|
};
|
||||||
|
const syncActivePlayer = v => {
|
||||||
|
activePlayerRef.current = v;
|
||||||
|
setActivePlayer(v);
|
||||||
|
};
|
||||||
|
const syncBombSelected = v => {
|
||||||
|
bombSelectedRef.current = v;
|
||||||
|
setBombSelected(v);
|
||||||
|
};
|
||||||
|
const syncConnLost = v => {
|
||||||
|
connectionLostRef.current = v;
|
||||||
|
setConnectionLost(v);
|
||||||
|
};
|
||||||
|
const syncRed = fn => {
|
||||||
|
const n = fn(redRef.current);
|
||||||
|
redRef.current = n;
|
||||||
|
setRed(n);
|
||||||
|
};
|
||||||
|
const syncBlue = fn => {
|
||||||
|
const n = fn(blueRef.current);
|
||||||
|
blueRef.current = n;
|
||||||
|
setBlue(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Overlay ───────────────────────────────────────────────────────────────
|
||||||
|
const showOverlay = (title, sub) => {
|
||||||
|
setOverlay(true);
|
||||||
|
setOverlayTitle(title);
|
||||||
|
setOverlaySubTitle(sub);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideOverlay = () => setOverlay(false);
|
||||||
|
|
||||||
|
// ── Cell helpers ──────────────────────────────────────────────────────────
|
||||||
|
const applyRevealedCell = (cell, player, isMainCell = false) => {
|
||||||
|
const { row, col, value } = cell;
|
||||||
|
setCells(prev => {
|
||||||
|
if (prev[row][col].active) return prev;
|
||||||
|
const patch = 'm' === value
|
||||||
|
? { currentImage: IMAGES.flag(player), currentObj: 'm', active: true }
|
||||||
|
: { currentImage: value, currentObj: value, active: true };
|
||||||
|
if (isMainCell) {
|
||||||
|
patch.lastClickedRed = 'red' === player;
|
||||||
|
patch.lastClickedBlue = 'blue' === player;
|
||||||
|
}
|
||||||
|
return patchCells(prev, [{ row, col, ...patch }]);
|
||||||
|
});
|
||||||
|
if (isMainCell) lastClickedRef.current = { ...lastClickedRef.current, [player]: [row, col] };
|
||||||
|
};
|
||||||
|
|
||||||
|
const showLeftMines = (leftMines = []) => {
|
||||||
|
if (!leftMines.length) return;
|
||||||
|
setCells(prev => {
|
||||||
|
const patches = leftMines
|
||||||
|
.filter(({ row, col }) => !prev[row][col].active)
|
||||||
|
.map(({ row, col }) => ({ row, col, currentImage: IMAGES.leftMine }));
|
||||||
|
return patches.length ? patchCells(prev, patches) : prev;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Game logic ────────────────────────────────────────────────────────────
|
||||||
|
const changePlayer = () => {
|
||||||
|
const wasColor = activePlayerRef.current ? 'blue' : 'red';
|
||||||
|
const nextColor = activePlayerRef.current ? 'red' : 'blue';
|
||||||
|
const nextVal = activePlayerRef.current ? 0 : 1;
|
||||||
|
const desc = wasColor === webPlayerRef.current ? DESC.buddy : DESC.you;
|
||||||
|
|
||||||
|
syncActivePlayer(nextVal);
|
||||||
|
syncRed(p => ({ ...p, active: 'red' === nextColor, desc: 'red' === nextColor ? desc : '' }));
|
||||||
|
syncBlue(p => ({ ...p, active: 'blue' === nextColor, desc: 'blue' === nextColor ? desc : '' }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyStep = stepData => {
|
||||||
|
const { player, bomb: isBomb, minesFound = 0, revealedCells = [], redPoints: rp, bluePoints: bp } = stepData;
|
||||||
|
|
||||||
|
if (isBomb) {
|
||||||
|
sounds.current.bomb.play();
|
||||||
|
} else if (0 < minesFound) {
|
||||||
|
const cur = ('red' === player ? redRef : blueRef).current.mines;
|
||||||
|
sounds.current[20 < cur + minesFound ? 'warning' : 'mine'].play();
|
||||||
|
} else {
|
||||||
|
sounds.current.click.play();
|
||||||
|
}
|
||||||
|
|
||||||
|
const lc = lastClickedRef.current[player];
|
||||||
|
setCells(prev => {
|
||||||
|
let next = prev;
|
||||||
|
if (lc) next = patchCells(next, [{ row: lc[0], col: lc[1], lastClickedRed: false, lastClickedBlue: false }]);
|
||||||
|
|
||||||
|
revealedCells.forEach(({ row, col, value }, idx) => {
|
||||||
|
if (next[row][col].active) return;
|
||||||
|
const patch = 'm' === value
|
||||||
|
? { currentImage: IMAGES.flag(player), currentObj: 'm', active: true }
|
||||||
|
: { currentImage: value, currentObj: value, active: true };
|
||||||
|
if (0 === idx) {
|
||||||
|
patch.lastClickedRed = 'red' === player;
|
||||||
|
patch.lastClickedBlue = 'blue' === player;
|
||||||
|
}
|
||||||
|
next = patchCells(next, [{ row, col, ...patch }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isBomb) next = next.map(r => r.map(c => null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c));
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (0 < revealedCells.length) {
|
||||||
|
lastClickedRef.current = { ...lastClickedRef.current, [player]: [revealedCells[0].row, revealedCells[0].col] };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 < minesFound) {
|
||||||
|
setMines(51 - rp - bp);
|
||||||
|
setFoundMines(true);
|
||||||
|
setTimeout(() => setFoundMines(false), 500);
|
||||||
|
syncRed(p => ({ ...p, mines: 'red' === player ? rp : p.mines }));
|
||||||
|
syncBlue(p => ({ ...p, mines: 'blue' === player ? bp : p.mines }));
|
||||||
|
}
|
||||||
|
|
||||||
|
syncRed(p => ({ ...p, enabledBomb: rp <= bp }));
|
||||||
|
syncBlue(p => ({ ...p, enabledBomb: bp <= rp }));
|
||||||
|
|
||||||
|
if (isBomb || 0 === minesFound) changePlayer();
|
||||||
|
|
||||||
|
if (isBomb) {
|
||||||
|
syncBombSelected(false);
|
||||||
|
syncRed(p => 'red' === player ? { ...p, haveBomb: false } : p);
|
||||||
|
syncBlue(p => 'blue' === player ? { ...p, haveBomb: false } : p);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeGameEndIfItEnds = (bluePoints, redPoints, resign = false, leftMines = []) => {
|
||||||
|
const redWins = 25 < redPoints;
|
||||||
|
const blueWins = 25 < bluePoints;
|
||||||
|
|
||||||
|
if (redWins || blueWins || resign) {
|
||||||
|
sounds.current.won.play();
|
||||||
|
|
||||||
|
if (!resign) showOverlay((redWins ? 'Red' : 'Blue') + ' wins the game!', 'Play again!');
|
||||||
|
|
||||||
|
showLeftMines(leftMines);
|
||||||
|
syncActivePlayer(false);
|
||||||
|
syncRed(p => ({ ...p, desc: '' }));
|
||||||
|
syncBlue(p => ({ ...p, desc: '' }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resignProcess = color => {
|
||||||
|
const wp = webPlayerRef.current;
|
||||||
|
showOverlay(
|
||||||
|
color === wp ? 'You have been give up' : 'Your opponent has been resigned',
|
||||||
|
color === wp ? 'You LOSE!' : 'You WIN!',
|
||||||
|
);
|
||||||
|
endRef.current = true;
|
||||||
|
makeGameEndIfItEnds(0, 0, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBombToggle = () => {
|
||||||
|
const next = !bombSelectedRef.current;
|
||||||
|
syncBombSelected(next);
|
||||||
|
if (!next) {
|
||||||
|
setCells(prev => prev.map(r => r.map(c => null !== c.bombTargetArea ? { ...c, bombTargetArea: null } : c)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GameContext.Provider
|
||||||
|
value={{
|
||||||
|
// State (for rendering)
|
||||||
|
webPlayer, activePlayer, overlay, overlayTitle, overlaySubTitle,
|
||||||
|
mines, bombSelected, foundMines, red, blue, cells, gridReady, connectionLost,
|
||||||
|
// Setters needed by useServerComm
|
||||||
|
setCells, setGridReady,
|
||||||
|
// Refs (needed by useServerComm for async-safe reads)
|
||||||
|
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, endRef,
|
||||||
|
// Sync helpers
|
||||||
|
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
||||||
|
// Game logic called by useServerComm
|
||||||
|
showOverlay, hideOverlay,
|
||||||
|
applyRevealedCell, applyStep,
|
||||||
|
makeGameEndIfItEnds, resignProcess,
|
||||||
|
// UI action
|
||||||
|
onBombToggle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</GameContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
26
assets/js/mine-seeker/hooks/useGameRefs.jsx
Normal file
26
assets/js/mine-seeker/hooks/useGameRefs.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useRef } from 'react';
|
||||||
|
import { PLAYER_DEF } from '../utils/constants';
|
||||||
|
|
||||||
|
const useGameRefs = () => {
|
||||||
|
const webPlayerRef = useRef(null);
|
||||||
|
const activePlayerRef = useRef(false);
|
||||||
|
const bombSelectedRef = useRef(false);
|
||||||
|
const connectionLostRef = useRef(false);
|
||||||
|
const redRef = useRef({ ...PLAYER_DEF });
|
||||||
|
const blueRef = useRef({ ...PLAYER_DEF });
|
||||||
|
const lastClickedRef = useRef({ red: null, blue: null });
|
||||||
|
const endRef = useRef(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
webPlayerRef,
|
||||||
|
activePlayerRef,
|
||||||
|
bombSelectedRef,
|
||||||
|
connectionLostRef,
|
||||||
|
redRef,
|
||||||
|
blueRef,
|
||||||
|
lastClickedRef,
|
||||||
|
endRef,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGameRefs;
|
||||||
36
assets/js/mine-seeker/hooks/useGameState.jsx
Normal file
36
assets/js/mine-seeker/hooks/useGameState.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { initCells, PLAYER_DEF } from '../utils/constants';
|
||||||
|
|
||||||
|
const useGameState = () => {
|
||||||
|
const [webPlayer, setWebPlayer] = useState(null);
|
||||||
|
const [activePlayer, setActivePlayer] = useState(false);
|
||||||
|
const [overlay, setOverlay] = useState(true);
|
||||||
|
const [overlayTitle, setOverlayTitle] = useState('');
|
||||||
|
const [overlaySubTitle, setOverlaySubTitle] = useState('');
|
||||||
|
const [mines, setMines] = useState(51);
|
||||||
|
const [bombSelected, setBombSelected] = useState(false);
|
||||||
|
const [foundMines, setFoundMines] = useState(false);
|
||||||
|
const [red, setRed] = useState({ ...PLAYER_DEF });
|
||||||
|
const [blue, setBlue] = useState({ ...PLAYER_DEF });
|
||||||
|
const [cells, setCells] = useState(initCells);
|
||||||
|
const [gridReady, setGridReady] = useState(false);
|
||||||
|
const [connectionLost, setConnectionLost] = useState(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
webPlayer, setWebPlayer,
|
||||||
|
activePlayer, setActivePlayer,
|
||||||
|
overlay, setOverlay,
|
||||||
|
overlayTitle, setOverlayTitle,
|
||||||
|
overlaySubTitle, setOverlaySubTitle,
|
||||||
|
mines, setMines,
|
||||||
|
bombSelected, setBombSelected,
|
||||||
|
foundMines, setFoundMines,
|
||||||
|
red, setRed,
|
||||||
|
blue, setBlue,
|
||||||
|
cells, setCells,
|
||||||
|
gridReady, setGridReady,
|
||||||
|
connectionLost, setConnectionLost,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGameState;
|
||||||
@@ -1,26 +1,22 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { useGame } from '../contexts/GameContext';
|
import { useGame } from '../contexts/GameContext';
|
||||||
import { DESC } from '../constants';
|
import { DESC } from '../utils/constants';
|
||||||
|
|
||||||
/**
|
/** Handles all server communication: SSE (Mercure), REST calls, and the initialization lifecycle. */
|
||||||
* Handles all server communication: SSE (Mercure), REST calls, and the
|
const useServerCommunication = (gameAssoc, gameInherited, isEnvDev) => {
|
||||||
* initialization lifecycle. Exposes only the UI-facing callbacks the
|
|
||||||
* component needs: onClick, resign.
|
|
||||||
*/
|
|
||||||
const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
|
|
||||||
const {
|
const {
|
||||||
// Async-safe refs
|
/** Async-safe refs */
|
||||||
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, endRef,
|
webPlayerRef, activePlayerRef, bombSelectedRef, connectionLostRef, endRef,
|
||||||
// State setters
|
/** State setters */
|
||||||
setGridReady,
|
setGridReady,
|
||||||
// Sync helpers
|
/** Sync helpers */
|
||||||
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
syncWebPlayer, syncActivePlayer, syncBombSelected, syncConnLost, syncRed, syncBlue,
|
||||||
// Game logic
|
/** Game logic */
|
||||||
showOverlay, hideOverlay,
|
showOverlay, hideOverlay,
|
||||||
applyRevealedCell, applyStep,
|
applyRevealedCell, applyStep,
|
||||||
makeGameEndIfItEnds, resignProcess,
|
makeGameEndIfItEnds, resignProcess,
|
||||||
// Current cells snapshot (for active-check in onClick)
|
/** Current cells snapshot (for active-check in onClick) */
|
||||||
cells,
|
cells,
|
||||||
} = useGame();
|
} = useGame();
|
||||||
|
|
||||||
@@ -28,7 +24,7 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
const rpcUsersRef = useRef(null);
|
const rpcUsersRef = useRef(null);
|
||||||
const stepCacheRef = useRef([]);
|
const stepCacheRef = useRef([]);
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
/** HELPERS */
|
||||||
|
|
||||||
const correctGridSize = () => {
|
const correctGridSize = () => {
|
||||||
let $f = $('#mine-wrapper .grid');
|
let $f = $('#mine-wrapper .grid');
|
||||||
@@ -38,7 +34,7 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
$('#mine-wrapper .grid .field-wrapper .field').height($f.width()).css('line-height', ($f.width() - 2) + 'px');
|
$('#mine-wrapper .grid .field-wrapper .field').height($f.width()).css('line-height', ($f.width() - 2) + 'px');
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── REST mutations / queries ──────────────────────────────────────────────
|
/** REST mutations / queries */
|
||||||
|
|
||||||
const connectQuery = useQuery({
|
const connectQuery = useQuery({
|
||||||
queryKey: ['game-connect', gameAssoc],
|
queryKey: ['game-connect', gameAssoc],
|
||||||
@@ -72,7 +68,7 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
}).then(r => r.json()),
|
}).then(r => r.json()),
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Game-start helpers (triggered by server events) ───────────────────────
|
/** Game-start helpers (triggered by server events) */
|
||||||
|
|
||||||
const wInit = (revealedCells = []) => {
|
const wInit = (revealedCells = []) => {
|
||||||
setGridReady(true);
|
setGridReady(true);
|
||||||
@@ -91,17 +87,20 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
|
|
||||||
const makeGameStart = payload => {
|
const makeGameStart = payload => {
|
||||||
syncActivePlayer(1);
|
syncActivePlayer(1);
|
||||||
syncRed(p => ({ ...p, name: payload.users.red || payload.users.redAnon || p.name }));
|
syncRed(p => ({
|
||||||
|
...p,
|
||||||
|
name: payload.users.red || payload.users.redAnon || p.name,
|
||||||
|
}));
|
||||||
syncBlue(p => ({
|
syncBlue(p => ({
|
||||||
...p,
|
...p,
|
||||||
name: payload.users.blue || payload.users.blueAnon || p.name,
|
name: payload.users.blue || payload.users.blueAnon || p.name,
|
||||||
desc: 'blue' === webPlayerRef.current ? DESC.you : DESC.buddy,
|
desc: 'blue' === webPlayerRef.current ? DESC.you : DESC.buddy,
|
||||||
active: true,
|
active: true,
|
||||||
}));
|
}));
|
||||||
hideOverlay();
|
hideOverlay();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Mercure / SSE message handlers ───────────────────────────────────────
|
/** Mercure / SSE message handlers */
|
||||||
|
|
||||||
const wSubscribe = (payload, rpcUsers = null) => {
|
const wSubscribe = (payload, rpcUsers = null) => {
|
||||||
isEnvDev && console.info((payload.user ?? 'user') + ' subscribed');
|
isEnvDev && console.info((payload.user ?? 'user') + ' subscribed');
|
||||||
@@ -174,17 +173,26 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
es.onmessage = e => handleMercureMessage(JSON.parse(e.data));
|
es.onmessage = e => handleMercureMessage(JSON.parse(e.data));
|
||||||
es.onopen = () => {
|
es.onopen = () => {
|
||||||
isEnvDev && console.info('SSE opened');
|
isEnvDev && console.info('SSE opened');
|
||||||
if (connectionLostRef.current) { isEnvDev && console.info('SSE reconnected'); joinMutation.mutate(); }
|
if (connectionLostRef.current) {
|
||||||
|
isEnvDev && console.info('SSE reconnected');
|
||||||
|
joinMutation.mutate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
es.onerror = () => {
|
||||||
|
isEnvDev && console.error('SSE error');
|
||||||
|
syncConnLost(true);
|
||||||
};
|
};
|
||||||
es.onerror = () => { isEnvDev && console.error('SSE error'); syncConnLost(true); };
|
|
||||||
eventSourceRef.current = es;
|
eventSourceRef.current = es;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Initialization ────────────────────────────────────────────────────────
|
/** Initialization */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
if (connectionLostRef.current) { openEventSource(); return; }
|
if (connectionLostRef.current) {
|
||||||
|
openEventSource();
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (gameInherited) {
|
if (gameInherited) {
|
||||||
const serverData = await connectQuery.refetch().then(r => {
|
const serverData = await connectQuery.refetch().then(r => {
|
||||||
@@ -219,7 +227,7 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ── UI-facing callbacks ───────────────────────────────────────────────────
|
/** UI-facing callbacks */
|
||||||
|
|
||||||
const onClick = async coords => {
|
const onClick = async coords => {
|
||||||
const activeColor = activePlayerRef.current ? 'blue' : 'red';
|
const activeColor = activePlayerRef.current ? 'blue' : 'red';
|
||||||
@@ -230,7 +238,10 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
|
|
||||||
const dataPack = { coords, player: activeColor, bomb: bombSelectedRef.current, resign: null };
|
const dataPack = { coords, player: activeColor, bomb: bombSelectedRef.current, resign: null };
|
||||||
|
|
||||||
if (connectionLostRef.current) { stepCacheRef.current.push(dataPack); return; }
|
if (connectionLostRef.current) {
|
||||||
|
stepCacheRef.current.push(dataPack);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await stepMutation.mutateAsync(dataPack);
|
const result = await stepMutation.mutateAsync(dataPack);
|
||||||
@@ -261,4 +272,4 @@ const useServerComm = (gameAssoc, gameInherited, isEnvDev) => {
|
|||||||
return { onClick, resign };
|
return { onClick, resign };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useServerComm;
|
export default useServerCommunication;
|
||||||
@@ -10,6 +10,21 @@ export const WAVES = {
|
|||||||
3: 'bg-wave-2-outbg.png',
|
3: 'bg-wave-2-outbg.png',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const IMAGES = {
|
||||||
|
target: `${IMG}bg-target-outbg.png`,
|
||||||
|
bomb: `${IMG}bg-bomb-outbg.png`,
|
||||||
|
bombDisabled: `${IMG}bg-bomb-disabled-outbg.png`,
|
||||||
|
bombExploded: `${IMG}bg-bomb-exploded-outbg.png`,
|
||||||
|
bombEmpty: `${IMG}bg-bomb-empty-outbg.png`,
|
||||||
|
leftMine: `${IMG}bg-left-mine-outbg.png`,
|
||||||
|
cursor: color => `${IMG}bg-cursor-${color}-outbg.png`,
|
||||||
|
figure: color => `${IMG}bg-figure-${color}-outbg.png`,
|
||||||
|
flag: player => `${IMG}bg-flag-${player}-outbg.png`,
|
||||||
|
last: color => `${IMG}bg-last-${color}-outbg.png`,
|
||||||
|
wave: n => `${IMG}${WAVES[n]}`,
|
||||||
|
bombPos: (hor, vert) => `${IMG}bg-bomb-${hor}-${vert}-outbg.png`,
|
||||||
|
};
|
||||||
|
|
||||||
export const PLAYER_DEF = {
|
export const PLAYER_DEF = {
|
||||||
name: '...', desc: '', active: false, mines: 0, haveBomb: true, enabledBomb: true,
|
name: '...', desc: '', active: false, mines: 0, haveBomb: true, enabledBomb: true,
|
||||||
};
|
};
|
||||||
@@ -53,7 +68,7 @@ export const patchCells = (prev, patches) => {
|
|||||||
export const initCells = () =>
|
export const initCells = () =>
|
||||||
Array.from({ length: ROWS }, () =>
|
Array.from({ length: ROWS }, () =>
|
||||||
Array.from({ length: COLS }, () => ({
|
Array.from({ length: COLS }, () => ({
|
||||||
currentImage: IMG + WAVES[Math.floor(Math.random() * 3) + 1],
|
currentImage: IMAGES.wave(Math.floor(Math.random() * 3) + 1),
|
||||||
currentObj: 'w',
|
currentObj: 'w',
|
||||||
active: false,
|
active: false,
|
||||||
lastClickedRed: false,
|
lastClickedRed: false,
|
||||||
@@ -7,12 +7,6 @@ export default [
|
|||||||
{
|
{
|
||||||
ignores: ['node_modules/**', 'vendor/**', 'var/**', 'public/build/**'],
|
ignores: ['node_modules/**', 'vendor/**', 'var/**', 'public/build/**'],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
files: ['**/*.jsx'],
|
|
||||||
rules: {
|
|
||||||
'react/jsx-uses-vars': 'error',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
files: ['**/*.{js,mjs,cjs,jsx,ts,tsx}'],
|
files: ['**/*.{js,mjs,cjs,jsx,ts,tsx}'],
|
||||||
plugins: {
|
plugins: {
|
||||||
@@ -111,4 +105,17 @@ export default [
|
|||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.jsx'],
|
||||||
|
rules: {
|
||||||
|
'react/jsx-uses-vars': 'error',
|
||||||
|
'no-unused-vars': ['error', {
|
||||||
|
varsIgnorePattern: '^React$',
|
||||||
|
args: 'after-used',
|
||||||
|
caughtErrors: 'none',
|
||||||
|
ignoreRestSiblings: false,
|
||||||
|
reportUsedIgnorePattern: false,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user