diff options
Diffstat (limited to '')
-rw-r--r-- | client/src/net/game.ts | 186 |
1 files changed, 186 insertions, 0 deletions
diff --git a/client/src/net/game.ts b/client/src/net/game.ts new file mode 100644 index 0000000..c8e5991 --- /dev/null +++ b/client/src/net/game.ts @@ -0,0 +1,186 @@ +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) { + console.log('input', frame, 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) { + console.log('data', frame, data) + 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<number>, + _desyncCheck: (frame: number) => Promise<boolean>, + ) => { + 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) + + } +} |