Add character emotes across bot friend and ranked matches

This commit is contained in:
2026-10-14 23:56:32 +09:00
parent c798753418
commit 493af700aa
14 changed files with 177 additions and 7 deletions
+17 -4
View File
@@ -4,6 +4,8 @@ import type { ActiveMatch, LocalMatchState, MatchCheckpoint, RecordAck, ResultSu
import type { ActionId } from '../shared/types.js';
import type { SessionStorage } from './result-reporter.js';
import { createFriendMatch, type GamePeer } from './friend-match.js';
import { createEmotes } from '../shared/emotes.js';
import type { EmoteId } from '../../constants/emotes.js';
export interface GameClock { now(): number; setTimeout(fn: () => void, ms: number): any; clearTimeout(id: any): void }
export interface ControllerConfig {
@@ -20,12 +22,14 @@ export function createMatchController(config: ControllerConfig) {
const engine = createMatchEngine({ ...config.activeMatch, selfIndex: config.selfIndex, clock: { now: () => clock.now() + offset }, random: config.random });
let friend: ReturnType<typeof createFriendMatch> | undefined;
const listeners = new Set<(state: LocalMatchState) => void>();
const emotes = createEmotes();
const view = () => ({ ...engine.view(), appearances: config.activeMatch.appearances, emotes: emotes.view(clock.now() + offset) });
let timer: any, destroyed = false, started = false, recording = false;
function publish() {
if (destroyed) return;
const checkpoint: ControllerCheckpoint = { gameSessionId: config.gameSessionId, activeMatch: config.activeMatch, checkpoint: engine.checkpoint(), epoch: 0, ...friend?.snapshot() };
config.storage.setItem(GAME_CHECKPOINT_KEY, JSON.stringify(checkpoint));
for (const listener of listeners) listener({ ...engine.view(), appearances: config.activeMatch.appearances });
for (const listener of listeners) listener(view());
}
async function record() {
const state = engine.view(); if (!state.finished || state.finished.saved || recording || destroyed) return;
@@ -63,7 +67,8 @@ export function createMatchController(config: ControllerConfig) {
if (config.activeMatch.mode === 'friend') {
if (!config.peer) throw new Error('PEER_REQUIRED');
friend = createFriendMatch({ matchId: config.activeMatch.matchId, slot: config.selfIndex, engine, peer: config.peer, clock,
setOffset: value => { offset = value; }, changed: publish, finished: () => { void record(); }, saved });
setOffset: value => { offset = value; }, changed: publish, finished: () => { void record(); }, saved,
onEmote: id => { if (emotes.send(config.selfIndex === 0 ? 1 : 0, id, clock.now() + offset)) publish(); } });
await friend.start(); return;
}
if (!saved) engine.start();
@@ -86,8 +91,16 @@ export function createMatchController(config: ControllerConfig) {
await config.onRematch?.();
if (!destroyed) friend?.requestRematch();
},
view: () => ({ ...engine.view(), appearances: config.activeMatch.appearances }),
subscribe(listener: (state: LocalMatchState) => void) { listeners.add(listener); listener({ ...engine.view(), appearances: config.activeMatch.appearances }); return () => { listeners.delete(listener); }; },
sendEmote(id: EmoteId) {
if (destroyed || !started || engine.view().phase === 'finished' || friend?.recovering) return false;
const now = clock.now() + offset;
if (config.activeMatch.mode === 'friend' && !config.peer?.connected) return false;
if (!emotes.send(config.selfIndex, id, now)) return false;
if (friend && !friend.sendEmote(id)) return false;
publish(); return true;
},
view,
subscribe(listener: (state: LocalMatchState) => void) { listeners.add(listener); listener(view()); return () => { listeners.delete(listener); }; },
retrySave: record,
destroy() { destroyed = true; clock.clearTimeout(timer); friend?.destroy(); listeners.clear(); },
};