Files
incremental-game-engine/src/components/Topbar.tsx
T

80 lines
2.3 KiB
TypeScript

import type Decimal from 'break_eternity.js';
import { useEffect, useRef } from 'react';
import type { PointerEvent } from 'react';
interface Props {
time: Decimal;
onTick: () => void;
onReset: () => void;
onInitialize: () => void;
}
const HOLD_TICK_INTERVAL_MS = 1000;
const MIN_TICK_INTERVAL_MS = 50;
export default function Topbar({ time, onTick, onReset, onInitialize }: Props) {
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Mutable, not React state: doTick reschedules itself via setTimeout, so it must always read
// and write the CURRENT speed directly — a value captured in a render's closure (e.g. via
// useState) would stay frozen for the lifetime of that closure, since nothing ever forces
// doTick to be re-created from a fresh render while a hold is in progress.
const speedRef = useRef(HOLD_TICK_INTERVAL_MS);
function stopHolding() {
if (timeoutRef.current !== null) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
speedRef.current = HOLD_TICK_INTERVAL_MS;
}
function doTick() {
onTick();
speedRef.current = Math.max(MIN_TICK_INTERVAL_MS, Math.pow(speedRef.current, 0.97));
timeoutRef.current = setTimeout(doTick, speedRef.current);
}
function startHolding(e: PointerEvent<HTMLButtonElement>) {
if (e.button !== 0)
return;
e.currentTarget.setPointerCapture(e.pointerId);
stopHolding();
doTick();
}
useEffect(() => {
window.addEventListener('blur', stopHolding);
return () => {
window.removeEventListener('blur', stopHolding);
stopHolding();
};
}, []);
return (
<div className="topbar">
<h1 className="topbar__title">
Граф ресурсов
<span>детерминированная тиковая симуляция</span>
</h1>
<div className="tick-counter">
Время <b>{time.toStringWithDecimalPlaces(4)}</b>
</div>
<div className="spacer" />
<button className="btn" onClick={onInitialize}>
Initialize
</button>
<button className="btn" onClick={onReset}>
Reset
</button>
<button
className="btn btn--primary"
onPointerDown={startHolding}
onPointerUp={stopHolding}
onPointerCancel={stopHolding}
>
Tick ▶
</button>
</div>
);
}