import { Frame, GameState, Input, Key, KeyMap, PlayerInput } from "../types.js"; import { startInputListener } from "./input.js"; import { multiplayer } from "./multiplayer.js"; /** * @author tint * @template Data, Input */ export class Game { historysize: number history: Frame[] historyStart: number currentFrame: number advance: (pastData: GameState, input: Input, frame: number) => GameState constructor(history: number) { this.historysize = history; this.history = []; this.historyStart = 0; // the game may have inputs from the "future" // (local input delay to make sure inputs play at the same time on all machines) // so the "present" isn't always the latest frame // the game loop should set this every frame this.currentFrame = 0; } startHistory(frame: number, data: GameState) { this.historyStart = frame; this.history = [{ data, input: { players: {} }}]; this.currentFrame = frame; } getHistory(frame: number): Frame { return this.history[frame - this.historyStart]; } getFrame(): number { return this.historyStart + this.history.length - 1; } getCurrentData(): GameState { const entry = this.history[this.history.length - 1]; return entry && entry.data; } /** * Sets the input at a specific frame. If that frame is in history, * the game will be rewound, the input applied, and then fast-forwarded to the current head. * If the frame is ahead of the current latest frame, the game will be run until that frame. */ setInput(frame: number, input: Input) { this.editFrame(frame, (index: number): void => { let past = this.history[index - 1]; if(index === 0) { past = { data: undefined, input: undefined }; } this.history[index] = { input, data: this.advance(past ? past.data : undefined, input, frame), }; }); } setData(frame: number, data: GameState) { this.editFrame(frame, (index: number): void => { this.history[index] = { data, input: this.history[index] && this.history[index].input, } }); } editFrame(frame: number, edit: (index: number) => void) { const head = this.historyStart + this.history.length; if(frame < head) { if(frame < this.historyStart) { throw new Error("Tried to edit a past frame not in history: " + frame); } edit(frame - this.historyStart); // fast forward back to the present with the new data for(let i = frame + 1; i < head; i++) { const past = this.history[i - this.historyStart - 1]; this.history[i - this.historyStart].data = this.advance( past ? past.data : undefined, this.history[i - this.historyStart].input, i ); } } else { // fast forward the inbetween frames with no input for(let i = head; i < frame; i++) { const entry = this.history[i - this.historyStart - 1]; this.history[i - this.historyStart] = { input: undefined, data: this.advance(entry ? entry.data : undefined, undefined, i), }; } edit(frame - this.historyStart); } while(this.history.length > this.historysize) { this.history.shift(); this.historyStart++; } } start ( code: string, keymap: KeyMap, onLoad: (startFrame: Frame) => boolean, onFrame: (data: Frame, frame: number) => void, onLogic: (pastData: GameState, input: Input, frame: number) => GameState, data: PlayerInput = { start: false, key: Key.NOTHING } ): void { const fps = 60; let delay = 3; this.advance = onLogic const onStart = ( startFrame: number, latency: number, _connection: number, update: (input: PlayerInput, frame: number) => void, _ping: () => Promise, _desyncCheck: (frame: number) => Promise, ) => { console.log("started game at frame", startFrame); // window.desyncCheck = () => desyncCheck(this.currentFrame - 5); let startTs = performance.now() - latency; let lastFrame = startFrame; update(data, startFrame + 1); let getInput = startInputListener(keymap) const startData = this.getHistory(startFrame) if (!onLoad(startData)) return false let lastTs = performance.now(); let loop = (ts: number) => { const frame = Math.floor((ts - startTs) / 1000 * fps) + startFrame; if(frame !== lastFrame) { // update input once per frame, regardless of the display refresh rate lastFrame = frame; // gather input const input: PlayerInput = getInput(); // apply input update(input, frame + delay); } this.currentFrame = frame const data = this.getHistory(frame) onFrame(data, frame) lastTs = ts requestAnimationFrame(loop) } requestAnimationFrame(loop) if(startFrame === -1) { update(data, 0); } return true } multiplayer(this, code, onStart) } }