blob: d6f049697366e7ade72dc378f6a13224206a3c2b [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert} from 'chrome://resources/js/assert.js';
import {DEFAULT_DIMENSIONS, FPS, IS_HIDPI} from './constants.js';
import type {ConfigProvider} from './game_config.js';
import type {GameStateProvider} from './game_state_provider.js';
import type {GeneratedSoundFxProvider} from './generated_sound_fx.js';
import type {ImageSpriteProvider} from './image_sprite_provider.js';
import {CollisionBox} from './offline_sprite_definitions.js';
import type {SpritePosition} from './sprite_position.js';
import {getTimeStamp} from './utils.js';
interface BaseTrexConfig {
dropVelocity: number;
flashOff: number;
flashOn: number;
height: number;
heightDuck: number;
introDuration: number;
speedDropCoefficient: number;
spriteWidth: number;
startXPos: number;
invertJump: boolean;
width: number;
widthDuck: number;
widthCrashed?: number;
widthJump?: number;
}
interface TrexJumpConfig {
gravity: number;
maxJumpHeight: number;
minJumpHeight: number;
initialJumpVelocity: number;
}
type TrexConfig = BaseTrexConfig&TrexJumpConfig;
interface TrexSpritePosition {
x: number;
w: number;
h: number;
xOffset: number;
}
// Sprite config for alternative game modes.
export type AltGameModeSpriteConfig = TrexConfig&{
jumping: TrexSpritePosition,
crashed: TrexSpritePosition,
running1: TrexSpritePosition,
running2: TrexSpritePosition,
ducking1: TrexSpritePosition,
ducking2: TrexSpritePosition,
collisionBoxes: CollisionBox[],
};
/**
* T-rex player config.
*/
const defaultTrexConfig: BaseTrexConfig = {
dropVelocity: -5,
flashOff: 175,
flashOn: 100,
height: 47,
heightDuck: 25,
introDuration: 1500,
speedDropCoefficient: 3,
spriteWidth: 262,
startXPos: 50,
width: 44,
widthDuck: 59,
invertJump: false,
};
const slowJumpConfig: TrexJumpConfig = {
gravity: 0.25,
maxJumpHeight: 50,
minJumpHeight: 45,
initialJumpVelocity: -20,
};
const normalJumpConfig: TrexJumpConfig = {
gravity: 0.6,
maxJumpHeight: 30,
minJumpHeight: 30,
initialJumpVelocity: -10,
};
/**
* Used in collision detection.
*/
const collisionBoxes: {ducking: CollisionBox[], running: CollisionBox[]} = {
ducking: [new CollisionBox(1, 18, 55, 25)],
running: [
new CollisionBox(22, 0, 17, 16),
new CollisionBox(1, 18, 30, 9),
new CollisionBox(10, 35, 14, 8),
new CollisionBox(1, 24, 29, 5),
new CollisionBox(5, 30, 21, 4),
new CollisionBox(9, 34, 15, 4),
],
};
export enum Status {
CRASHED,
DUCKING,
JUMPING,
RUNNING,
WAITING,
}
/**
* Blinking coefficient.
*/
const BLINK_TIMING: number = 7000;
interface FrameInfo {
frames: number[];
msPerFrame: number;
}
/**
* Animation config for different states.
*/
const animFrames: Record<Status, FrameInfo> = {
[Status.WAITING]: {
frames: [44, 0],
msPerFrame: 1000 / 3,
},
[Status.RUNNING]: {
frames: [88, 132],
msPerFrame: 1000 / 12,
},
[Status.CRASHED]: {
frames: [220],
msPerFrame: 1000 / 60,
},
[Status.JUMPING]: {
frames: [0],
msPerFrame: 1000 / 60,
},
[Status.DUCKING]: {
frames: [264, 323],
msPerFrame: 1000 / 8,
},
};
export class Trex {
config: TrexConfig;
playingIntro: boolean = false;
xPos: number = 0;
yPos: number = 0;
jumpCount: number = 0;
ducking: boolean = false;
blinkCount: number = 0;
jumping: boolean = false;
speedDrop: boolean = false;
private canvasCtx: CanvasRenderingContext2D;
private spritePos: SpritePosition;
private xInitialPos: number = 0;
// Position when on the ground.
private groundYPos: number = 0;
private currentFrame: number = 0;
private currentAnimFrames: number[] = [];
private blinkDelay: number = 0;
private animStartTime: number = 0;
private timer: number = 0;
private msPerFrame: number = 1000 / FPS;
// Current status.
private status: Status = Status.WAITING;
private jumpVelocity: number = 0;
private reachedMinHeight: boolean = false;
private altGameModeEnabled: boolean = false;
private flashing: boolean = false;
private minJumpHeight: number;
private resourceProvider: ConfigProvider&GameStateProvider&
ImageSpriteProvider&GeneratedSoundFxProvider;
/**
* T-rex game character.
*/
constructor(
canvas: HTMLCanvasElement, spritePos: SpritePosition,
resourceProvider: ConfigProvider&GameStateProvider&ImageSpriteProvider&
GeneratedSoundFxProvider) {
const canvasContext = canvas.getContext('2d');
assert(canvasContext);
this.canvasCtx = canvasContext;
this.spritePos = spritePos;
this.resourceProvider = resourceProvider;
this.config = Object.assign(defaultTrexConfig, normalJumpConfig);
const runnerDefaultDimensions = DEFAULT_DIMENSIONS;
const runnerBottomPadding = this.resourceProvider.getConfig().bottomPad;
assert(runnerDefaultDimensions);
assert(runnerBottomPadding);
this.groundYPos = runnerDefaultDimensions.height - this.config.height -
runnerBottomPadding;
this.yPos = this.groundYPos;
this.minJumpHeight = this.groundYPos - this.config.minJumpHeight;
this.draw(0, 0);
this.update(0, Status.WAITING);
}
/**
* Assign the appropriate jump parameters based on the game speed.
*/
enableSlowConfig() {
const jumpConfig =
this.resourceProvider.hasSlowdown ? slowJumpConfig : normalJumpConfig;
this.config = Object.assign(defaultTrexConfig, jumpConfig);
this.adjustAltGameConfigForSlowSpeed();
}
/**
* Enables the alternative game. Redefines the dino config.
* @param spritePos New positioning within image sprite.
*/
enableAltGameMode(spritePos: SpritePosition) {
this.altGameModeEnabled = true;
this.spritePos = spritePos;
const spriteDefinition = this.resourceProvider.getSpriteDefinition();
assert(spriteDefinition);
const tRexSpriteDefinition =
spriteDefinition.tRex as AltGameModeSpriteConfig;
assert(tRexSpriteDefinition.running1);
const runnerDefaultDimensions = DEFAULT_DIMENSIONS;
// Update animation frames.
animFrames[Status.RUNNING].frames =
[tRexSpriteDefinition.running1.x, tRexSpriteDefinition.running2.x];
animFrames[Status.CRASHED].frames = [tRexSpriteDefinition.crashed.x];
if (typeof tRexSpriteDefinition.jumping.x === 'object') {
animFrames[Status.JUMPING].frames = tRexSpriteDefinition.jumping.x;
} else {
animFrames[Status.JUMPING].frames = [tRexSpriteDefinition.jumping.x];
}
animFrames[Status.DUCKING].frames =
[tRexSpriteDefinition.ducking1.x, tRexSpriteDefinition.ducking2.x];
// Update Trex config
this.config.gravity = tRexSpriteDefinition.gravity || this.config.gravity;
this.config.height = tRexSpriteDefinition.running1.h,
this.config.initialJumpVelocity = tRexSpriteDefinition.initialJumpVelocity;
this.config.maxJumpHeight = tRexSpriteDefinition.maxJumpHeight;
this.config.minJumpHeight = tRexSpriteDefinition.minJumpHeight;
this.config.width = tRexSpriteDefinition.running1.w;
this.config.widthCrashed = tRexSpriteDefinition.crashed.w;
this.config.widthJump = tRexSpriteDefinition.jumping.w;
this.config.invertJump = tRexSpriteDefinition.invertJump;
this.adjustAltGameConfigForSlowSpeed(tRexSpriteDefinition.gravity);
// Adjust bottom horizon placement.
this.groundYPos = runnerDefaultDimensions.height - this.config.height -
spriteDefinition.bottomPad;
this.yPos = this.groundYPos;
this.reset();
}
/**
* Slow speeds adjustments for the alt game modes.
*/
private adjustAltGameConfigForSlowSpeed(gravityValue?: number) {
if (this.resourceProvider.hasSlowdown) {
if (gravityValue) {
this.config.gravity = gravityValue / 1.5;
}
this.config.minJumpHeight *= 1.5;
this.config.maxJumpHeight *= 1.5;
this.config.initialJumpVelocity *= 1.5;
}
}
/**
* Setter whether dino is flashing.
*/
setFlashing(status: boolean) {
this.flashing = status;
}
/**
* Setter for the jump velocity.
* The appropriate drop velocity is also set.
*/
setJumpVelocity(setting: number) {
this.config.initialJumpVelocity = -setting;
this.config.dropVelocity = -setting / 2;
}
/**
* Set the animation status.
*/
update(deltaTime: number, status?: Status) {
this.timer += deltaTime;
// Update the status.
if (status !== undefined) {
this.status = status;
this.currentFrame = 0;
this.msPerFrame = animFrames[status].msPerFrame;
this.currentAnimFrames = animFrames[status].frames;
if (status === Status.WAITING) {
this.animStartTime = getTimeStamp();
this.setBlinkDelay();
}
}
// Game intro animation, T-rex moves in from the left.
if (this.playingIntro && this.xPos < this.config.startXPos) {
this.xPos += Math.round(
(this.config.startXPos / this.config.introDuration) * deltaTime);
this.xInitialPos = this.xPos;
}
if (this.status === Status.WAITING) {
this.blink(getTimeStamp());
} else {
this.draw(this.currentAnimFrames[this.currentFrame]!, 0);
}
// Update the frame position.
if (!this.flashing && this.timer >= this.msPerFrame) {
this.currentFrame =
this.currentFrame === this.currentAnimFrames.length - 1 ?
0 :
this.currentFrame + 1;
this.timer = 0;
}
// Speed drop becomes duck if the down key is still being pressed.
if (this.speedDrop && this.yPos === this.groundYPos) {
this.speedDrop = false;
this.setDuck(true);
}
}
/**
* Draw the t-rex to a particular position.
*/
draw(x: number, y: number) {
let sourceX = x;
let sourceY = y;
let sourceWidth = this.ducking && this.status !== Status.CRASHED ?
this.config.widthDuck :
this.config.width;
let sourceHeight = this.config.height;
const outputHeight = sourceHeight;
if (this.altGameModeEnabled) {
assert(this.config.widthCrashed);
}
const outputWidth =
this.altGameModeEnabled && this.status === Status.CRASHED ?
this.config.widthCrashed! :
this.config.width;
const runnerImageSprite = this.resourceProvider.getRunnerImageSprite();
assert(runnerImageSprite);
// Width of sprite can change on jump or crashed.
if (this.altGameModeEnabled) {
if (this.jumping && this.status !== Status.CRASHED) {
assert(this.config.widthJump);
sourceWidth = this.config.widthJump;
} else if (this.status === Status.CRASHED) {
assert(this.config.widthCrashed);
sourceWidth = this.config.widthCrashed;
}
}
if (IS_HIDPI) {
sourceX *= 2;
sourceY *= 2;
sourceWidth *= 2;
sourceHeight *= 2;
}
// Adjustments for sprite sheet position.
sourceX += this.spritePos.x;
sourceY += this.spritePos.y;
// Flashing.
if (this.flashing) {
if (this.timer < this.config.flashOn) {
this.canvasCtx.globalAlpha = 0.5;
} else if (this.timer > this.config.flashOff) {
this.timer = 0;
}
}
// Ducking.
if (this.ducking && this.status !== Status.CRASHED) {
this.canvasCtx.drawImage(
runnerImageSprite, sourceX, sourceY, sourceWidth, sourceHeight,
this.xPos, this.yPos, this.config.widthDuck, outputHeight);
} else if (
this.altGameModeEnabled && this.jumping &&
this.status !== Status.CRASHED) {
assert(this.config.widthJump);
const spriteDefinition = this.resourceProvider.getSpriteDefinition();
assert(spriteDefinition);
assert(spriteDefinition.tRex);
const jumpOffset =
spriteDefinition.tRex.jumping.xOffset * (IS_HIDPI ? 2 : 1);
// Jumping with adjustments.
this.canvasCtx.drawImage(
runnerImageSprite, sourceX, sourceY, sourceWidth, sourceHeight,
this.xPos - jumpOffset, this.yPos, this.config.widthJump,
outputHeight);
} else {
// Crashed whilst ducking. Trex is standing up so needs adjustment.
if (this.ducking && this.status === Status.CRASHED) {
this.xPos++;
}
// Standing / running
this.canvasCtx.drawImage(
runnerImageSprite, sourceX, sourceY, sourceWidth, sourceHeight,
this.xPos, this.yPos, outputWidth, outputHeight);
}
this.canvasCtx.globalAlpha = 1;
}
/**
* Sets a random time for the blink to happen.
*/
private setBlinkDelay() {
this.blinkDelay = Math.ceil(Math.random() * BLINK_TIMING);
}
/**
* Make t-rex blink at random intervals.
* @param time Current time in milliseconds.
*/
private blink(time: number) {
const deltaTime = time - this.animStartTime;
if (deltaTime >= this.blinkDelay) {
this.draw(this.currentAnimFrames[this.currentFrame]!, 0);
if (this.currentFrame === 1) {
// Set new random delay to blink.
this.setBlinkDelay();
this.animStartTime = time;
this.blinkCount++;
}
}
}
/**
* Initialise a jump.
*/
startJump(speed: number) {
if (!this.jumping) {
this.update(0, Status.JUMPING);
// Tweak the jump velocity based on the speed.
this.jumpVelocity = this.config.initialJumpVelocity - (speed / 10);
this.jumping = true;
this.reachedMinHeight = false;
this.speedDrop = false;
if (this.config.invertJump) {
this.minJumpHeight = this.groundYPos + this.config.minJumpHeight;
}
}
}
/**
* Jump is complete, falling down.
*/
endJump() {
if (this.reachedMinHeight && this.jumpVelocity < this.config.dropVelocity) {
this.jumpVelocity = this.config.dropVelocity;
}
}
/**
* Update frame for a jump.
*/
updateJump(deltaTime: number) {
const msPerFrame = animFrames[this.status].msPerFrame;
const framesElapsed = deltaTime / msPerFrame;
// Speed drop makes Trex fall faster.
if (this.speedDrop) {
this.yPos += Math.round(
this.jumpVelocity * this.config.speedDropCoefficient * framesElapsed);
} else if (this.config.invertJump) {
this.yPos -= Math.round(this.jumpVelocity * framesElapsed);
} else {
this.yPos += Math.round(this.jumpVelocity * framesElapsed);
}
this.jumpVelocity += this.config.gravity * framesElapsed;
// Minimum height has been reached.
if (this.config.invertJump && (this.yPos > this.minJumpHeight) ||
!this.config.invertJump && (this.yPos < this.minJumpHeight) ||
this.speedDrop) {
this.reachedMinHeight = true;
}
// Reached max height.
if (this.config.invertJump && (this.yPos > -this.config.maxJumpHeight) ||
!this.config.invertJump && (this.yPos < this.config.maxJumpHeight) ||
this.speedDrop) {
this.endJump();
}
// Back down at ground level. Jump completed.
if ((this.config.invertJump && (this.yPos < this.groundYPos)) ||
(!this.config.invertJump && (this.yPos > this.groundYPos))) {
this.reset();
this.jumpCount++;
if (this.resourceProvider.hasAudioCues) {
const generatedSoundFx = this.resourceProvider.getGeneratedSoundFx();
assert(generatedSoundFx);
generatedSoundFx.loopFootSteps();
}
}
}
/**
* Set the speed drop. Immediately cancels the current jump.
*/
setSpeedDrop() {
this.speedDrop = true;
this.jumpVelocity = 1;
}
setDuck(isDucking: boolean) {
if (isDucking && this.status !== Status.DUCKING) {
this.update(0, Status.DUCKING);
this.ducking = true;
} else if (this.status === Status.DUCKING) {
this.update(0, Status.RUNNING);
this.ducking = false;
}
}
/**
* Reset the t-rex to running at start of game.
*/
reset() {
this.xPos = this.xInitialPos;
this.yPos = this.groundYPos;
this.jumpVelocity = 0;
this.jumping = false;
this.ducking = false;
this.update(0, Status.RUNNING);
this.speedDrop = false;
this.jumpCount = 0;
}
getCollisionBoxes(): CollisionBox[] {
return this.ducking ? collisionBoxes.ducking : collisionBoxes.running;
}
}