base2020/src/game/GameComponents.ts

227 lines
5.9 KiB
TypeScript

import { KeyName } from "../applet/Keyboard";
import { DrawSet, Layer } from "../applet/Render";
import { ComponentSchema, Data as EcsData, FixedPoint } from "../ecs/Components";
import { Component, copySparse, Join, StateForSchema, Store } from "../ecs/Data";
import { DumbMotion } from "../ecs/Location";
import { LockstepProcessor, TICK_LENGTH } from "../ecs/Lockstep";
import { Buttons } from "./Input";
export enum GamePhase {
TITLE,
PLAYING,
LOST,
WON
}
export type RGB = [number, number, number];
export class World {
width = 500 as FixedPoint;
height = 400 as FixedPoint;
/*
* Core Game Status
*/
phase = GamePhase.TITLE;
score = 0;
constructor() { }
/*
* Drawing Layers
*/
groundLayer = new Layer(0);
debugLayer = new Layer(2);
bulletLayer = new Layer(10);
playerLayer = new Layer(15);
smokeLayer = new Layer(16);
hudLayer = new Layer(20);
bgColor: RGB = [255, 255, 255];
/**
* Catch-all debug tool
*/
debug: Record<string, any> = {};
drawHud(drawSet: DrawSet) {
drawSet.queue(this.hudLayer.toRender((cx, dt) => {
cx.font = "16px monospace";
cx.textAlign = "left";
cx.textBaseline = "middle";
const score = `Score: ${this.score}`;
cx.fillStyle = "#000";
cx.fillText(score, this.width / 3 + 1, this.height - 18 + 1, this.width / 4);
cx.fillStyle = "#0ff";
cx.fillText(score, this.width / 3, this.height - 18, this.width / 4);
}));
}
}
interface GameSchema extends ComponentSchema {
boss: Boss;
bullet: Bullet;
hp: Hp;
lifetime: Lifetime;
message: Message;
}
export class Data extends EcsData implements StateForSchema<GameSchema> {
boss: Store<Boss>;
bullet: Store<Bullet>;
hp: Store<Hp>;
lifetime: Store<Lifetime>;
message: Store<Message>;
playerControl: Store<PlayerControl>;
// globals
debugLayer = new Layer(2);
constructor(from: Partial<Data>) {
super(from);
this.boss = copySparse(from.boss);
this.bullet = copySparse(from.bullet);
this.hp = copySparse(from.hp);
this.lifetime = copySparse(from.lifetime);
this.message = copySparse(from.message);
this.playerControl = copySparse(from.playerControl);
}
clone() {
return new Data(this);
}
}
export enum Teams {
PLAYER,
ENEMY
}
export class Bullet extends Component<Bullet> {
hit: boolean;
team: Teams;
attack: number;
constructor(from: Partial<Bullet>) {
super(from);
this.hit = from.hit ?? false;
this.team = from.team ?? Teams.ENEMY;
this.attack = from.attack ?? 1;
}
clone(): Bullet {
return new Bullet(this);
}
}
export class Hp extends Component<Hp> {
receivedDamage: number;
team: Teams;
hp: number;
constructor(from: Partial<Hp>) {
super(from);
this.receivedDamage = from.receivedDamage ?? 0;
this.team = from.team ?? Teams.ENEMY;
this.hp = from.hp ?? 10;
}
clone(): Hp {
return new Hp(this);
}
}
export class Lifetime extends Component<Lifetime> {
time: number;
constructor(from: Partial<Lifetime> & { time: number }) {
super(from);
this.time = from.time;
}
clone(): Lifetime {
return new Lifetime(this);
}
}
export class Boss extends Component<Boss> {
name: string;
constructor(from: Partial<Boss>) {
super(from);
this.name = from.name ?? "";
}
clone(): Boss {
return new Boss(this);
}
}
export class Message extends Component<Message> {
targetY = 0 as FixedPoint;
layer: number;
color: string;
message: string;
timeout = 3;
constructor(from: Partial<Message>) {
super(from);
this.targetY = from.targetY ?? (0 as FixedPoint);
this.layer = from.layer ?? 1;
this.color = from.color ?? "#000";
this.message = from.message ?? "";
this.timeout = from.timeout ?? 3;
}
clone(): Message {
return new Message(this);
}
}
export class PlayerControl extends Component<PlayerControl> {
playerNumber: number;
constructor(from: Partial<PlayerControl> & {playerNumber: number}) {
super(from);
this.playerNumber = from.playerNumber;
}
clone(): PlayerControl {
return new PlayerControl(this);
}
}
export class Engine implements LockstepProcessor<KeyName[], KeyName[][], Data> {
cloneState(old: Data) {
return new Data(old);
}
predictInput(prev: KeyName[][] = [], localPlayer: number, localInput: KeyName[]): KeyName[][] {
const prediction = prev.slice();
for(let i = 0; i <= localPlayer; i++) {
if(!prediction[i]) {
prediction[i] = [];
}
}
prediction[localPlayer] = localInput;
return prediction;
}
compareInput(a: KeyName[][], b: KeyName[][]): boolean {
if (a.length != b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i].length != b[i].length) return false;
for (let j = 0; j < a[i].length; j++) {
if (a[i][j] != b[i][j]) {
return false;
}
}
}
return true;
}
advanceState(state: Data, input: KeyName[][]) {
DumbMotion(state, TICK_LENGTH);
Join(state, "playerControl", "location").forEach(([player, location]) => {
const playerInput = input[player.playerNumber];
if(playerInput) {
let dir = 0;
if (playerInput.indexOf("left") != -1) {
dir -= 1;
}
if (playerInput.indexOf("right") != -1) {
dir += 1;
}
location.VAngle = (dir * 0.01) as FixedPoint;
}
});
}
}