111 lines
3 KiB
JavaScript
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++;
|
|
}
|
|
}
|
|
}
|