tuxman/client/js/game.js
2023-06-13 21:18:01 -04:00

111 lines
3 KiB
JavaScript

/**
* @author tint
* @template Data, Input
*/
export class Game {
/**
* @param {number} history How many frames of history to keep in memory
* @param {(data: Data, input: Input, frame: number) => Data} advance The function to apply game logic. For rollback to work properly, this must be a pure function, and can't mutate inputs.
*/
constructor(history, advance) {
this.historysize = history;
this.advance = advance;
/** @type {{data: Data, input: Input}[]} */
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, data) {
this.historyStart = frame;
this.history = [{ data }];
this.currentFrame = frame;
}
getHistory(frame) {
return this.history[frame - this.historyStart];
}
getFrame() {
return this.historyStart + this.history.length - 1;
}
getCurrentData() {
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.
* @param {number} frame The time to apply the input at
* @param {Input} input The input
*/
setInput(frame, input) {
this.editFrame(frame, index => {
let past = this.history[index - 1];
if(index === 0) {
past = { data: undefined };
}
this.history[index] = {
input,
data: this.advance(past ? past.data : undefined, input, frame),
};
});
}
setData(frame, data) {
this.editFrame(frame, index => {
this.history[index] = {
data,
input: this.history[index] && this.history[index].input,
}
});
}
/**
* @param {number} frame
* @param {(index: number) => void} edit
*/
editFrame(frame, edit) {
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++;
}
}
}