Release v0.1.20 2026-07-19

This commit is contained in:
Warren H
2026-07-19 16:37:37 -04:00
parent 437e70fc58
commit 018e060cdd
22 changed files with 884 additions and 121 deletions
+18 -77
View File
@@ -51,12 +51,11 @@ import { DualDisplayFrame } from "./DualDisplayFrame";
import { DEFAULT_CONTROLLER_GLYPHS } from "../input/controllerGlyphs";
import {
HOCKEY_PVP_GOAL_DAMAGE,
HOCKEY_PVP_COUNTDOWN_MS,
HOCKEY_PVP_QUEUE_TIMEOUT_MS,
hockeyPvpBossAt,
randomHockeyPvpCpuName,
type HockeyPvpMatchConfig,
} from "../game/hockeyHealingPvp";
import { startHockeyPvpMatchmaking, type HockeyPvpMatchOperation } from "../frontend/hockeyPvpMatchmaking";
import { BLOCKBREAKER_BREACH_DAMAGE } from "../game/blockbreaker";
import {
APPEARANCE_SLOT_DEFINITIONS,
@@ -1545,10 +1544,7 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi
const [queueing, setQueueing] = useState(false);
const [queueElapsed, setQueueElapsed] = useState(0);
const queueActive = useRef(false);
const queueTicket = useRef<string | null>(null);
const queuePollTimer = useRef<number | null>(null);
const queueCpuTimer = useRef<number | null>(null);
const queueClockTimer = useRef<number | null>(null);
const queueOperation = useRef<HockeyPvpMatchOperation | null>(null);
const mode = MODE_COPY[modeId];
const healer = hunter ? HEALER_CLASSES[hunter.activeClassId] : HEALER_CLASSES.priest;
const progress = hunter?.healers[hunter.activeClassId];
@@ -1568,42 +1564,19 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi
const selectBossGroup = (groupId: (typeof BOSS_GROUPS)[number]["id"]) => {
selectBoss(BOSS_GROUP_BY_ID[groupId].bossIds[0]);
};
const clearQueueTimers = () => {
if (queuePollTimer.current !== null) window.clearTimeout(queuePollTimer.current);
if (queueCpuTimer.current !== null) window.clearTimeout(queueCpuTimer.current);
if (queueClockTimer.current !== null) window.clearInterval(queueClockTimer.current);
queuePollTimer.current = null;
queueCpuTimer.current = null;
queueClockTimer.current = null;
};
const completePvpQueue = (match: HockeyPvpMatchConfig) => {
if (!queueActive.current) return;
queueActive.current = false;
clearQueueTimers();
queueOperation.current = null;
setQueueing(false);
setMessage(match.role === "cpu" ? `CPU rival found: ${match.opponentName}.` : `Matched with ${match.opponentName}.`);
onLaunch([hockeyPvpBossAt(match.seed, 0)], "initiate", match);
};
const fallbackToCpu = () => {
if (!queueActive.current) return;
const ticketId = queueTicket.current;
queueTicket.current = null;
if (ticketId) void onlineRepository.cancelHockeyPvpQueue(ticketId).catch(() => undefined);
completePvpQueue({
matchId: null,
seed: Math.max(1, Math.floor(Math.random() * 0xffffffff)),
opponentName: randomHockeyPvpCpuName(),
role: "cpu",
countdownEndsAtMs: Date.now() + HOCKEY_PVP_COUNTDOWN_MS,
});
};
const cancelPvpQueue = () => {
if (!queueActive.current) return;
queueActive.current = false;
clearQueueTimers();
const ticketId = queueTicket.current;
queueTicket.current = null;
if (ticketId) void onlineRepository.cancelHockeyPvpQueue(ticketId).catch(() => undefined);
queueOperation.current?.cancel();
queueOperation.current = null;
setQueueing(false);
setQueueElapsed(0);
setMessage("Matchmaking cancelled.");
@@ -1614,54 +1587,22 @@ function ModeScreen({ onLaunch }: { onLaunch: (bossIds: readonly BossId[], diffi
setQueueing(true);
setQueueElapsed(0);
setMessage(accountId ? "Searching online queue…" : "Offline queue: searching before CPU fallback…");
const startedAt = Date.now();
queueClockTimer.current = window.setInterval(() => setQueueElapsed(Date.now() - startedAt), 100);
queueCpuTimer.current = window.setTimeout(fallbackToCpu, HOCKEY_PVP_QUEUE_TIMEOUT_MS);
if (!accountId || !networkAppearsOnline()) return;
try {
const joined = await onlineRepository.joinHockeyPvpQueue(hunter.slotId, hunter.hunterName);
if (!queueActive.current) return;
queueTicket.current = joined.ticketId;
if (joined.match) {
completePvpQueue({
matchId: joined.match.id,
seed: joined.match.seed,
countdownEndsAtMs: joined.match.countdownEndsAtMs,
opponentName: joined.match.opponentName,
role: joined.match.role,
});
return;
}
const poll = async () => {
if (!queueActive.current || !queueTicket.current) return;
try {
const result = await onlineRepository.pollHockeyPvpQueue(queueTicket.current);
if (!queueActive.current) return;
if (result.match) {
completePvpQueue({
matchId: result.match.id,
seed: result.match.seed,
countdownEndsAtMs: result.match.countdownEndsAtMs,
opponentName: result.match.opponentName,
role: result.match.role,
});
return;
}
} catch {
// Five-second CPU fallback remains authoritative during transient outages.
}
if (queueActive.current) queuePollTimer.current = window.setTimeout(poll, 350);
};
queuePollTimer.current = window.setTimeout(poll, 350);
} catch {
setMessage("Online queue unavailable. CPU fallback still searching…");
}
const operation = startHockeyPvpMatchmaking({
slotId: hunter.slotId,
hunterName: hunter.hunterName,
online: Boolean(accountId && networkAppearsOnline()),
onElapsed: setQueueElapsed,
onOnlineUnavailable: () => setMessage("Online queue unavailable. CPU fallback still searching…"),
});
queueOperation.current = operation;
const match = await operation.result;
if (!match || queueOperation.current !== operation) return;
completePvpQueue(match);
};
useEffect(() => () => {
queueActive.current = false;
clearQueueTimers();
const ticketId = queueTicket.current;
if (ticketId) void onlineRepository.cancelHockeyPvpQueue(ticketId).catch(() => undefined);
queueOperation.current?.cancel();
queueOperation.current = null;
}, []);
const leaveMode = () => {
cancelPvpQueue();